PageSourceSearch

https://disfrutalasmatematicas.com/main4a.js

js disfrutalasmatematicas.com collected 2026-10-02 11:50:46 UTC 69,494 bytes, 2,226 lines download raw bytes

1// dlm
2// https://www.dlm.com/main4a.js https://www.dlm.com/style4.css
3
4/* global reSpell, Author, mainMsg, loginDo */
5
6let main = { theme: 'lite', version: '0.804', timePrev: performance.now(), searchScript: 'no', searchStatus: 'no', searchData: '', name: '', stamp: '', loginQ: false, iframes: [], adsense:'ca-pub-1389989178296449', dom: 'dlm', server: 'www.mathbro.com', port: '3003' }
7
8if (location.hostname === 'localhost') main.port = '3004'
9
10// console.log('document.lastModified', document.lastModified)
11
12let menuVis = false
13let searchVis = false
14// console.log('menuVis', menuVis)
15// localization
16let userLang = window.navigator.userLanguage || window.navigator.language
17
18main.decSep = (1.5).toLocaleString().charAt(1)
19main.thouSep = String.fromCharCode(90 - main.decSep.charCodeAt(0))
20if (main.decSep == ',') {
21	main.decType = 'c'
22} else {
23	main.decType = ''
24}
25//console.log('DecSep',DecSep,ThouSep,userLang)
26
27function pathAbsolute(base, relative) {
28	let stack = base.split('/'),
29		parts = relative.split('/')
30	stack.pop() // remove current file name (or empty string)
31	// (omit if "base" is the current folder without trailing slash)
32	for (let i = 0; i < parts.length; i++) {
33		if (parts[i] == '.') continue
34		if (parts[i] == '..') stack.pop()
35		else stack.push(parts[i])
36	}
37	return stack.join('/')
38}
39
40function decfmt() {
41	if (main.decSep == ',') {
42		// 0,00=>0#00 then 0.0=>0,0, then 0#0=>0.0
43		fixSpells(document.body, 'd')
44		//document.body.innerHTML = document.body.innerHTML.replace(/(\d),(\d\d)/g, '$1#$2').replace(/(\d)\.(\d)/g, '$1,$2').replace(/(\d)#(\d)/g, '$1.$2');
45		// images replace "." with "c."
46		let imgs = document.body.getElementsByTagName('img')
47		for (let i = 0; i < imgs.length; i++) {
48			if (imgs[i].getAttribute('hasdec') != null && imgs[i].getAttribute('hasdec') != '') {
49				imgs[i].src = imgs[i].src.replace(/\.(gif|jpg|png)/g, 'c.$1')
50			}
51		}
52	}
53}
54
55function doSpell() {
56	if (typeof reSpell == 'undefined') return
57	let userLang = window.navigator.userLanguage || window.navigator.language
58
59	switch (userLang.toLowerCase()) {
60		case 'en-us':
61			break
62		case 'en-au':
63		case 'en-ca':
64		case 'en-gb':
65		case 'en-ie':
66		case 'en-nz':
67		case 'en-za':
68			fixSpells(document.body, 's')
69			break
70		default:
71	}
72}
73
74function fixSpells(elem, tp) {
75	// check if parameter is a an ELEMENT_NODE
76	if (!(elem instanceof Node) || elem.nodeType !== Node.ELEMENT_NODE) return
77	let children = elem.childNodes
78	for (let i = 0; children[i]; ++i) {
79		let node = children[i]
80		switch (node.nodeType) {
81			case Node.ELEMENT_NODE: // call recursively
82				fixSpells(node, tp)
83				break
84			case Node.TEXT_NODE: // fix spelling
85				if (tp == 's') fixSpell(node)
86				if (tp == 'd') fixDec(node)
87				break
88		}
89	}
90}
91
92function fixSpell(node) {
93	let s = node.nodeValue
94	if (s.length < 4) return // leave quickly if small
95	if (!s.match(/(?=.*[a-zA-Z])/)) return // leave if no ordinary chars inside
96	//s = "(" + s + ")"
97	let sStt = s
98	for (let j = 0; j < reSpell.length; j++) {
99		let s0 = reSpell[j][0]
100		let s1 = reSpell[j][1]
101		s = s.replace(new RegExp('\\b' + s0 + '\\b', 'g'), s1)
102		s = s.replace(new RegExp('\\b' + proper(s0) + '\\b', 'g'), proper(s1))
103	}
104	if (s != sStt) node.nodeValue = s // only update if changed
105}
106
107function fixDec(node) {
108  let s = node.nodeValue;
109  let sStt = s;
110
111  s = s
112    // 1. Replace thousands commas with '#' using lookahead for 3-digit groups
113    .replace(/\b(\d+)(?:,(\d{3}))+\b/g, (match) => match.replace(/,/g, '#'))
114    // 2. Convert decimal point to Spanish comma
115    .replace(/(\d)\.(\d)/g, '$1,$2')
116    // 3. Convert temporary '#' placeholders to Spanish dots
117    .replace(/#/g, '.');
118
119  if (s !== sStt) {
120    node.nodeValue = s;
121  }
122}
123
124function fixDecz2(node) {
125  let s = node.nodeValue;
126  let sStt = s;
127
128  // Use a temporary placeholder character '#' that won't appear in text
129  s = s
130    // 1. Swap US thousands commas (1,234) with temporary placeholder '#'
131    .replace(/(\d),(\d{3})/g, '$1#$2')
132    // 2. Swap US decimal points (1.23) with Spanish decimal commas (1,23)
133    .replace(/(\d)\.(\d)/g, '$1,$2')
134    // 3. Swap placeholder '#' to Spanish thousands dots (1.234) or space
135    .replace(/(\d)#(\d{3})/g, '$1.$2');
136
137  if (s !== sStt) {
138    node.nodeValue = s;
139  }
140}
141
142function fixDecz1(node) {
143	let s = node.nodeValue
144	let sStt = s
145	s = s
146		.replace(/(\d),(\d\d)/g, '$1#$2')
147		.replace(/(\d)\.(\d)/g, '$1,$2')
148		.replace(/(\d)#(\d)/g, '$1.$2')
149
150
151	//if (s.match(/(?=.*[a-zA-Z])/)) { // any chars inside at all?
152	if (s != sStt) {
153		//console.log("fixDec",sStt,"=>",s);
154		node.nodeValue = s // only update if changed
155	}
156	//}
157}
158
159function doLocal() {
160	decfmt()
161	doSpell()
162	relatedLinks()
163}
164
165function eqFmts() {
166	let roots = document.querySelectorAll('p, li, h3, .large, .larger, .huge')
167	console.log('eqFmts roots', roots.length)
168	let chgN = 0
169	let sttTm = performance.now()
170
171	for (let root of roots) {
172		let walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
173
174		let nodes = []
175		while (walker.nextNode()) nodes.push(walker.currentNode)
176
177		for (let node of nodes) {
178			let s = node.textContent
179			if (!s.includes(' = ')) continue
180			// if (node.parentNode.children.length > 0) continue  // over excludes, because of <sup> etc
181
182			let span = document.createElement('span')
183			span.innerHTML = s.replace(/ = /g, ' <span class="eq">=</span> ')
184			chgN++
185			node.parentNode.replaceChild(span, node)
186		}
187	}
188	let elapsed = performance.now() - sttTm
189	console.log('eqFmts chgN', chgN, elapsed)
190}
191
192function proper(s) {
193	return s.charAt(0).toUpperCase() + s.substring(1, s.length).toLowerCase()
194}
195
196function tellAFriend() {
197	let msg = "\nI found '" + document.title + "' here: " + location.href + '\n'
198	window.location = 'mailto:?subject=' + encodeURIComponent(document.title) + '&body=' + encodeURIComponent(msg)
199}
200
201function addFavorites() {
202	if (window.sidebar) {
203		// Mozilla Firefox Bookmark
204		window.sidebar.addPanel(document.title, location.href, '')
205	} else if (window.external) {
206		// IE Favorite
207		window.external.AddFavorite(location.href, document.title)
208	}
209}
210
211function openEnglish() {
212	if (typeof tranfrom == 'undefined') tranfrom = 'index.htm'
213	let path = tranfrom // only relative path to avoid spoofing
214	let url = 'https://www.mathsisfun.com/' + path
215	window.location = url
216}
217
218function linkToUs() {
219	let dom = location.hostname
220	if (dom == 'localhost') dom = `localhost/${main.dom}`
221	let pgURL = dom + '/link-to-us.html'
222	pgURL += '?path=' + toHex(location.pathname + location.search)
223	pgURL += '&title=' + toHex(document.title)
224	console.log('dom', dom, pgURL)
225
226	window.location = 'https://' + pgURL
227}
228
229function donate() {
230	let pg = location.pathname
231	let pgHex = toHex(pg)
232	logSend(pgHex, 'donate', window.location.hostname)
233
234	let dom = location.hostname
235	if (dom == 'localhost') dom = `localhost/${main.dom}`
236	let pgURL = dom + '/donate.html'
237
238	window.location = 'https://' + pgURL
239}
240
241function citation() {
242	let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
243	let path = location.pathname + location.search // only relative path to avoid spoofing
244	let atitle = document.title
245	let md = new Date(document.lastModified)
246	let mDate = md.getDate() + ' ' + months[md.getMonth()] + ' ' + md.getFullYear()
247	// if (typeof Author == 'undefined') Author = 'Pierce, Rod'
248	let author = typeof Author == 'undefined' ? 'Pierce, Rod' : Author
249
250	let data = { path: path, title: atitle, moddate: mDate, author: author }
251	localStorage.setItem('citation', JSON.stringify(data))
252
253	let urlStt = urlSttGet()
254	let to = urlStt + 'citation.html'
255	console.log('citation', data, to)
256	window.open(to)
257	// postWith(to, data)
258}
259
260function postWith(to, p) {
261	// TOOD: fetch() API
262
263	// from http://mentaljetsam.wordpress.com
264	let myForm = document.createElement('form')
265	myForm.method = 'post'
266	myForm.action = to
267	for (let k in p) {
268		let myInput = document.createElement('input')
269		myInput.setAttribute('name', k)
270		myInput.setAttribute('value', p[k])
271		myForm.appendChild(myInput)
272	}
273	document.body.appendChild(myForm)
274	myForm.submit()
275	document.body.removeChild(myForm)
276}
277
278function URLEncode(text) {
279	// The Javascript escape and unescape functions do not correspond with what browsers actually do...
280	let SAFECHARS = '0123456789' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + "-_.!~*'()" // numeric,alpha, and RFC2396 Mar
280k characters
281	let HEX = '0123456789ABCDEF'
282
283	let s = ''
284	for (let i = 0; i < text.length; i++) {
285		let ch = text.charAt(i)
286		if (ch == ' ') {
287			s += '+' // x-www-urlencoded, rather than %20
288		} else if (SAFECHARS.indexOf(ch) != -1) {
289			s += ch
290		} else {
291			let charCode = ch.charCodeAt(0)
292			if (charCode > 255) {
293				// Unicode cannot be encoded using standard URL encoding
294				s += '+'
295			} else {
296				s += '%'
297				s += HEX.charAt((charCode >> 4) & 0xf)
298				s += HEX.charAt(charCode & 0xf)
299			}
300		}
301	}
302
303	return s
304}
305
306function copyToClipboard(txtArea) {
307	txtArea.focus()
308	txtArea.select()
309	let copiedTxt = document.selection.createRange()
310	copiedTxt.execCommand('Copy')
311}
312
313function toHex(s) {
314	let hex = ''
315	for (let i = 0; i < s.length; i++) {
316		let cc = s.charCodeAt(i).toString(16)
317		if (cc.length < 2) cc = '0' + cc
318		hex += '' + cc
319	}
320	return hex
321}
322
323/* ads */
324
325function adOffQ() {
326	let item = localStorage.getItem('adoff')
327	if (item === null) return false
328
329	let object = JSON.parse(item)
330	let dateString = object.when
331	let until = new Date(dateString)
332	// until.setTime(until.getTime() + 1 * 60 * 1000) // plus 1 minute
333	until.setTime(until.getTime() + 8 * 24 * 60 * 60 * 1000) // plus 8 days
334
335	let now = new Date()
336	//console.log('adOffGet', until, now, until > now)
337
338	return until > now
339}
340
341function adOffSet() {
342	let object = { value: false, when: new Date() }
343	localStorage.setItem('adoff', JSON.stringify(object))
344	location.reload() // reload page so "no ads" takes effect
345}
346
347function adOffReset() {
348	let object = { value: false, when: 0 }
349	localStorage.setItem('adoff', JSON.stringify(object))
350	location.reload() // reload page so it takes effect
351}
352
353function adsSet(onQ) {
354	let adIDs = [
355		{ id: 'adTop', withAdsQ: true },
356		{ id: 'adend', withAdsQ: true },
357		{ id: 'adsHide1', withAdsQ: true },
358		{ id: 'adsShow1', withAdsQ: false },
359	]
360
361	//let styles = ['none', 'inline'];
362	for (let i = 0; i < adIDs.length; i++) {
363		let ad = adIDs[i]
364		let div = document.getElementById(ad.id)
365		if (div) {
366			let showQ = ad.withAdsQ
367			if (!onQ) showQ = !showQ
368			if (showQ) {
369				div.style.display = 'inline'
370			} else {
371				div.style.display = 'none'
372			}
373			//console.log('adsSet', ad, div, showQ)
374		}
375	}
376}
377
378function adsHideHTML() {
379	let s = ''
380	s += '<div id="adsShow1"><a href="javascript:adsShow()">Show Ads</a></div>'
381	s += '<div id="adsHide1"><a href="javascript:adsHide()">Hide Ads</a> | '
382
383	let imgHome = location.hostname == 'localhost' ? `/${main.dom}/` : '/'
384	s += '<a href="' + imgHome + 'about-ads.html">About Ads</a> </div>'
385
386	return s
387}
388
389// do hide ads based on local storage
390function adsHideSet() {
391	if (adOffQ()) {
392		adsSet(false)
393	}
394}
395
396function adsHide() {
397	adsSet(false)
398	adOffSet()
399}
400
401function adsShow() {
402	adsSet(true)
403	adOffReset()
404}
405
406function adsDo() {
407	let adType = 'adsense'
408
409	if (location.hostname == 'localhost') adType = 'dummy'
410	if (location.hostname == '127.0.0.1') adType = 'dummy'
411
412	console.log('adsDo', location.hostname, adType)
413
414	if (adType == 'adsense') {
415		adsenseDo()
416	}
417
418	if (adType == 'dummy') {
419		adsDummy()
420	}
421}
422
423function adsDummy() {
424	let dummy = document.createElement('img')
425	dummy.setAttribute('src', `/${main.dom}/images/style/320x60.jpg`)
426	dummy.setAttribute('alt', 'dummy image')
427	dummy.setAttribute('width', '320')
428	dummy.setAttribute('height', '60')
429	console.log('adsDummy', adsDummy)
430
431	let dest = document.getElementById('adend')
432	dest.appendChild(dummy)
433}
434
435function adsenseDo() {
436	// as recommended by gemini ... testing!
437  if (document.body.classList.contains('minimal')) {
438    console.log('No Ads due to Minimal')
439    return
440  }
441
442  // 1. Inject main AdSense script if not already present
443  let script = document.querySelector('script[src*="adsbygoogle.js"]')
444  if (!script) {
445    script = document.createElement('script')
446    script.async = true
447    script.crossOrigin = 'anonymous'
448    script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adsense}}`
449    document.head.appendChild(script)
450  }
451
452  // 2. Target destination
453  let dest = document.getElementById('adend')
454  if (!dest) {
455    console.error("Element with id 'adend' not found!")
456    return
457  }
458
459  // 3. Create and append the <ins> tag
460  let ins = document.createElement('ins')
461  ins.className = 'adsbygoogle'
462  ins.style.display = 'block'
463  ins.dataset.adClient = adsense
464  ins.dataset.adSlot = '2009442555'
465  ins.dataset.adFormat = 'auto'
466  ins.dataset.fullWidthResponsive = 'true'
467  dest.appendChild(ins)
468
469  // 4. Trigger the ad push directly in JavaScript
470  ;(window.adsbygoogle = window.adsbygoogle || []).push({})
471
472  console.log('adsenseDo: done')
473}
474
475
476function adsenseDoRP() {
477	if (document.body.classList.contains('minimal')) {
478		console.log('No Ads due to Minimal')
479		return
480	}
481
482	// Create and append AdSense script, but only once
483	let script = document.querySelector('script[src*="adsbygoogle.js"]')
484	if (!script) {
485		script = document.createElement('script')
486		script.async = true
487		script.crossOrigin = 'anonymous'
488		// script.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1389989178296449'
489		script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adsense}}`
490
491		document.head.appendChild(script)
492	}
493
494	// Wait for script to load before pushing ad
495	//script.onload = () => {
496	let ins = document.createElement('ins')
497	ins.className = 'adsbygoogle'
498	ins.style.display = 'block'
499	ins.dataset.adClient = adsense
500	ins.dataset.adSlot = '2009442555'
501	ins.dataset.adFormat = 'auto'
502	ins.dataset.fullWidthResponsive = 'true'
503
504	let dest = document.getElementById('adend')
505	console.log('dest', dest)
506	if (dest) {
507		dest.appendChild(ins)
508
509		let inlineScript = document.createElement('script')
510		inlineScript.text = '(adsbygoogle = window.adsbygoogle || []).push({})'
511		dest.appendChild(inlineScript)
512
513		console.log('adsenseDo: done')
514	} else {
515		console.error("Element with id 'adend' not found!")
516	}
517}
518
519function printImg(s) {
520	window.open(s, '_blank')
521	setTimeout('pwin.print()', 2000)
522}
523
524/* Question Database */
525function doQ(qid, qs) {
526	let fromPath = location.pathname + location.search // only relative path to avoid spoofing
527	let title = document.title
528	let url = 'http://www.mathopolis.com/questions/q.html?qid=' + parseInt(qid) + '&t=mif'
529	if (typeof qs == 'undefined') {
530		// url += '&qs=0'
531	} else {
532		url += '&qs=' + qs
533	}
534	url += '&site=1' + '&ref=' + toHex(fromPath) + '&title=' + toHex(title)
535	window.open(url, 'mathopolis')
536}
537
538function doQLocal(qid, s) {
539	let wd = Math.min(window.innerWidth - 50, 800)
540	let urlStt = urlSttGet() // to cope with pages at root or one level down
541
542	console.log('wd', wd, s, '(' + urlStt + ')')
543	window.open(urlStt + 'quiz/q.html?qid=' + parseInt(qid) + '&qs=' + s, 'popup', 'width=' + wd + ',height=600,left=50')
544	// http://localhost/${main.dom}/quiz/q.html?678_679_1424_1425_1429_2149_2150_3765_3766_3767
545}
546
547function getQ() {
548	let qs = ''
549	for (let i = 0; i < arguments.length; i++) {
550		if (i > 0) qs += '_'
551		qs += arguments[i]
552	}
553	let s = ''
554	for (let i = 0; i < arguments.length; i++) {
555		s += '<a href="javascript:doQ(' + arguments[i] + ",'" + qs + "'" + ')">Question&nbsp;' + (i + 1) + '</a> '
556	}
557	document.write(s)
558}
559
560function getQueryVar(varName, defaultVal = '') {
561	let parms = new URLSearchParams(window.location.search)
562	let val = parms.get(varName)
563
564	return val !== null ? val : defaultVal
565}
566
567function urlSttGet() {
568	// let stack = location.href.split("/");
569	// stack.pop(); // remove current file name (or empty string)
570	// let url = stack.join("/"); // remove filename
571
572	let url = location.href
573
574	url = url.replace('localhost/', '') // first remove any 'localhost/' to have consistent '/' count
575	let slashN = url.split('/').length - 4 // next count '/', and subtract 3 from 'https://www.${main.dom}.com/' and 1 because split has extra 1
576
577	let urlStt = ''
578	for (let i = 0; i < slashN; i++) urlStt += '../'
579
580	//console.log('urlSttGet', location.href, slashN, url, urlStt)
581
582	return urlStt
583}
584
585function menuHTML(type) {
586	// cope with urls at root or down one folder  NB: what about URLs down two folders?
587
588	let urlStt = urlSttGet() // to cope with pages at root or one level down
589
590	let links = [
591		['index.html', 'Inicio', 0],
592		['algebra/index.html', 'Álgebra', 0],
593		['calculo/index.html', 'Cálculo', 1],
594		['datos/index.html', 'Datos', 0],
595		['geometria/index.html', 'Geometría', 0],
596		['medida/index.html', 'Medida', 0],
597		['dinero/index.html', 'Dinero', 1],
598		['numeros/index.html', 'Números', 0],
599		['fisica/index.html', 'Física', 0],
600		['actividades/index.html', 'Actividades', 1],
601		['definiciones/index.html', 'Diccionario', 0],
602		['juegos/index.html', 'Juegos', 0],
603		['puzzles/index.html', 'Puzzles', 0],
604		['ejercicios/index.php', 'Ejercicios', 1],
605	]
606
607	let s = ''
608
609	let linkLen = links.length
610	let i
611	if (type == 0) {
612		s += '<ul role="list">'
613		for (i = 0; i < linkLen; i++) {
614			if (links[i][2] == 0) {
615				s += '<li role="listitem" tabindex="0"><a href="' + urlStt + links[i][0] + '">' + links[i][1] + '</a></li>'
616				s += '\n'
617			}
618		}
619		//s += '<li role="listitem" tabindex="0">';
620		//s += '</li>';
621		s += '</ul>'
622	}
623	if (type == 1) {
624		// bar menu at top
625		s += '<div class="menuPop">'
626
627		let ht = 36
628
629		// close
630		s += '<div class="menuCol">'
631		s += `<a href="javascript:adsHide()">`
632		s += '<img src="' + urlStt + 'images/style/no.svg" style="height:' + ht + 'px; vertical-align:middle;" alt="Calculator" />'
633		s += '</a>'
634		s += '</div>'
635
636		// home
637		//s += '<li><a role="listitem" href="' + urlStt + links[0][0] + '">' + links[0][1] + '</a></li>'
638		s += '<div class="menuCol">'
639		s += '<a href="' + urlStt + 'index.htm" style="text-decoration: none;  padding:0px;" aria-label="Home">'
640		s += '<img src="' + urlStt + 'images/style/home.svg" style="height:' + ht + 'px; vertical-align:middle;" alt="Home" />'
641		s += '</a>'
642		s += '</div>'
643
644		// search
645		s += '<div class="menuCol">'
646		s += '<a href="' + urlStt + 'search/search.html" style="text-decoration: none; padding:0px;" aria-label="Show Search">'
647		s += '<img src="' + urlStt + 'images/style/search.svg" style="height:' + ht + 'px; vertical-align:middle;" alt="Search" />'
648		s += '</a>'
649		s += '</div>'
650
651		// subjects
652		s += '<div class="menuCol">'
653		for (i = 1; i <= 8; i++) {
654			s += '<a role="listitem" href="' + urlStt + links[i][0] + '">' + links[i][1] + '</a><br>'
655		}
656		s += '</div>'
657
658		// more
659		s += '<div class="menuCol">'
660		for (i = 9; i < links.length; i++) {
661			s += '<a role="listitem" href="' + urlStt + links[i][0] + '">' + links[i][1] + '</a><br>'
662		}
663		s += '</div>'
664
665		// Ads
666
667		// Determine root path based on hostname
668		let imgHome = location.hostname === 'localhost' ? `/${main.dom}/` : '/'
669
670		// Create Ads dropdown menu
671		s += '<div class="menuCol">'
672		s += `<a href="javascript:adsHide()">Hide Ads</a><br>`
673		s += `<a href="javascript:adsShow()">Show Ads</a><br>`
674		s += `<a href="${imgHome}about-ads.html">About Ads</a><br>`
675		s += `<a href="${imgHome}donate.html">Donate</a><br>`
676		s += '</div>'
677
678		// calculator
679		s += '<div class="menuCol">'
680		s += '<a href="' + urlStt + 'numbers/calculator.html" style="text-decoration: none;  padding:0px;" aria-label="Show Calculator">'
681		s += '<img src="' + urlStt + 'images/style/calculator.svg" style="height:' + ht + 'px; vertical-align:middle;" alt="Calculator" />'
682		s += '</a>'
683		s += '</div>'
684
685		s += '</div>'
686	}
687
688	if (type == 11) {
689		// bar menu at top
690		s += '<ul role="list" class="bga1" style="display:inline-block; margin-top:25px; padding:6px; border-radius:10px; border: 1px solid; z-index:2; box-shadow: 3px 2px; ">'
691
692		let ht = 36
693
694		// home
695		//s += '<li><a role="listitem" href="' + urlStt + links[0][0] + '">' + links[0][1] + '</a></li>'
696		s += '<li style=" padding:0px 7px;">'
697		s += '<a href="' + urlStt + 'index.htm" style="text-decoration: none;  padding:0px;" aria-label="Home">'
698		s += '<img src="' + urlStt + 'images/style/home.svg" style="height:' + ht + 'px; vertical-align:middle;" alt="Home" />'
699		s += '</a>'
700		s += '</li>'
701
702		// search
703		s += '<li style=" padding:0px 7px;">'
704		s += '<a href="' + urlStt + 'search/search.html" style="text-decoration: none; padding:0px;" aria-label="Show Search">'
705		s += '<img src="' + urlStt + 'images/style/search.svg" style="height:' + ht + 'px; vertical-align:middle;" alt="Search" />'
706		s += '</a>'
707		s += '</li>'
708
709		// subjects
710		s += '<li><a role="listitem" href="#">Subjects &#x25BC;</a>'
711		s += '<ul>'
712		for (i = 1; i <= 8; i++) {
713			s += '<li><a role="listitem" href="' + urlStt + links[i][0] + '">' + links[i][1] + '</a></li>'
714		}
715		s += '</ul>'
716		s += '</li>'
717
718		// more
719		s += '<li><a role="listitem" href="#">More &#x25BC;</a>'
720		s += '<ul role="list">'
721		for (i = 9; i < links.length; i++) {
722			s += '<li><a role="listitem" href="' + urlStt + links[i][0] + '">' + links[i][1] + '</a></li>'
723		}
724		s += '</ul>'
725		s += '</li>'
726
727		// Ads
728
729		// Determine root path based on hostname
730		let imgHome = location.hostname === 'localhost' ? `/${main.dom}/` : '/'
731
732		// Create Ads dropdown menu
733		s += '<li><a href="#">Ads &#x25BC;</a>'
734		s += '<ul>'
735		s += `<li><a href="javascript:adsHide()">Hide Ads</a></li>`
736		s += `<li><a href="javascript:adsShow()">Show Ads</a></li>`
737		s += `<li><a href="${imgHome}about-ads.html">About Ads</a></li>`
738		s += `<li><a href="${imgHome}donate.html">Donate</a></li>`
739		s += '</ul>'
740		s += '</li>'
741
742		// s += '<li><a role="listitem" href="#">Ads &#x25BC;</a>'
743		// s += '<ul>'
744
745		// //s += '<li><a role="listitem" href="' + urlStt + links[i][0] + '">' + links[i][1] + '</a></li>'
746		// let imgHome = location.hostname == 'localhost' ? `/${main.dom}/` : '/'
747
748		// s += '<li><a role="listitem" href="javascript:adsHide()">Hide</a></li>'
749		// s += '<li><a role="listitem" href="javascript:adsShow()">Show</a></li>'
750		// s += '<li><a href="' + imgHome + 'contribute.html">
750Contribute</a></li>'
751		// s += '<li><a href="' + imgHome + 'about-ads.html">About Ads</a></li>'
752		// // s += '<a href="' + imgHome + 'about-ads.html">About Ads</a> </div>'
753
754		// s += '</ul>'
755		// s += '</li>'
756
757		// s += '<li style="margin:0;  padding:0px 7px;">'
758		// s += '<a href="javascript:adsHide()" style="margin:0; padding:0px 0px;">Hide Ads</a>'
759		//s += '<br>'
760		//s += '<a href="javascript:adsHide()" style="margin:0;  padding:0px 0px;">Contribute</a>'
761
762		// s += adsHideHTML()
763		//		s += '</li>'
764
765		// calculator
766		s += '<li style=" padding:0px 7px;">'
767		s += '<a href="' + urlStt + 'numbers/calculator.html" style="text-decoration: none;  padding:0px;" aria-label="Show Calculator">'
768		s += '<img src="' + urlStt + 'images/style/calculator.svg" style="height:' + ht + 'px; vertical-align:middle;" alt="Calculator" />'
769		s += '</a>'
770		s += '</li>'
771
772		s += '</ul>'
773	}
774	if (type == 2) {
775		s += '<div id="login" style="display: inline-block; vertical-align:top; text-align:center; line-height:110%; padding:4px; ">'
776
777		s += '</div>'
778
779		s += ' '
780		s += '<a href="javascript:menuShow()" style="text-decoration: none;" aria-label="Show Menu">'
781		s += '<img src="' + urlStt + 'images/style/menu.svg" width="39" height="39" class="hov" alt="Menu" />'
782		s += '</a>'
783
784		s += '<div id="loginPop" class="pop" style="position:absolute; width:300px; left:200px; opacity:0;">'
785		s += '<div id="loginContent" style="width:95%; margin: 0 0 5px 10px;">&nbsp;</div>'
786		s += '<div id="loginMsg" style="width:95%; margin: 0 0 5px 10px;">&nbsp;</div>'
787		s += '<button onclick="loginHide()" style="z-index:2;" class="btn" >Close</button>'
788		s += '</div>'
789	}
790
791	// alert(s);
792	//console.log('getMenu',typ, s)
793	return s
794}
795
796function menuShow() {
797	console.log('menuShow', menuVis)
798	let menuDiv = document.getElementById('menuSlim')
799	let srchDiv = document.getElementById('search')
800	if (menuVis) {
801		menuDiv.style.display = 'none'
802		srchDiv.style.visibility = 'visible'
803		// srchDiv.style.display = 'block'
804	} else {
805		menuDiv.style.display = 'block'
806		srchDiv.style.visibility = 'hidden'
807		// srchDiv.style.display = 'none'
808		// if (searchVis) searchShow() // turn off
809	}
810	menuVis = !menuVis
811}
812
813function searchShow() {
814	console.log('searchShow', searchVis)
815	let div = document.getElementById('search')
816	if (searchVis) {
817		div.style.display = 'none'
818		div.style.position = 'relative'
819	} else {
820		div.style.display = 'block'
821		div.style.position = 'absolute'
822		if (menuVis) menuShow() // turn off
823	}
824	searchVis = !searchVis
825}
826
827function translateHTML() {
828	//return ''
829	let s = ''
830	//let url = location.href; //  safe?
831	s += '<div id="google_translate_element"></div>'
832	s += '<script type="text/javascript">'
833	s += '	function googleTranslateElementInit() {'
834	s += "new google.translate.TranslateElement({pageLanguage: 'en', layout: google.translate.TranslateElement.InlineLayout.SIMPLE}, 'google_translate_element');"
835	s += '}'
836	if (false) {
837		s += '</script>'
838		s += '	<script type="text/javascript" src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>'
839	} else {
840		console.log('async translate2')
841		s += 'let googleTranslateScript = document.createElement("script");'
842		s += 'googleTranslateScript.type = "text/javascript";'
843		s += 'googleTranslateScript.async = true;'
844		s += 'googleTranslateScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";'
845		s += '( document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0] ).appendChild(googleTranslateScript);'
846		s += '</script>'
847	}
848	return s
849}
850
851function cookMsg() {
852	let s = ''
853
854	let imgHome = location.hostname == 'localhost' ? `/${main.dom}/` : '/'
855	s += 'We may use <a href="' + imgHome + 'about-ads.html">Cookies</a> &nbsp;'
856	s += '<div class="btn" style="display:inline-block; cursor: pointer;" onclick="cookOK()">OK</div>'
857
858	return s
859}
860
861function cookOK() {
862	console.log('cookOK')
863	document.getElementById('cookOK').style.display = 'none'
864	localStorage.setItem('cookie', 'ok')
865}
866
867// function docInsert(s) {
868//   let div = document.createElement('div')
869//   div.innerHTML = s
870//   let script = document.currentScript
871//   script.parentElement.insertBefore(div, script); // Add the newly-created div before script location
872// }
873
874function linksHTML() {
875	const url = encodeURIComponent(location.href)
876	const title = encodeURIComponent(document.title)
877	const baseLink = (href, title, id) => `<a target="_blank" rel="noopener nofollow" href="${href}" title="${title}" id="${id}" aria-label="${title}"></a>`
878
879	// Define social media share URLs
880	let facebookLink = baseLink(`https://www.facebook.com/sharer/sharer.php?u=${url}&t=${title}`, 'Share on Facebook', 'linkfb')
881	let twitterLink = baseLink(`https://x.com/intent/tweet?url=${url}&text=${title}`, 'Tweet it on X', 'linktw')
882	let whatsappLink = baseLink(`https://wa.me/?text=${title} ${url}`, 'Share on WhatsApp', 'linkwa')
883	let pinterestLink = baseLink(`https://www.pinterest.com/pin/create/button/?url=${url}&description=${title}`, 'Pin it', 'linkpi')
884
885	// Construct links HTML
886	let links = ''
887	links += facebookLink
888	links += twitterLink
889	links += whatsappLink
890	//links += pinterestLink
891	links += '<a href="javascript:tellAFriend()" id="linkem" title="eMail a Friend" aria-label="eMail a Friend"></a>'
892	links += '<a href="javascript:linkToUs()" id="linkus" title="Link To Us" aria-label="Link To Us"></a>'
893	//links += '<a href="javascript:donate()" id="linkdo" title="Donate" aria-label="Donate"></a>'
894
895	return links
896}
897
898function footerHTML() {
899	let s = ''
900	let urlStt = urlSttGet()
901	let sep = ' &cir; '
902	s += '<div id="footMenu">'
903	s += '<a href="' + urlStt + 'search/search.html">Búsqueda</a>' + sep
904	s += '<a href="' + urlStt + 'index.html">Índice</a>' + sep
905	s += '<a href="' + urlStt + 'aboutus.html">Acerca de</a>' + sep
906	s += '<a href="' + urlStt + 'contact.html">Contacto</a>' + sep
907	s += '<a href="javascript:citation()">Cita&nbsp;esta&nbsp;página</a>' + sep
908	s += '<b><a href="' + urlStt + 'privacy.html">Privacidad</a></b>'
909	s += '</div>'
910	return s
911}
912
913function themeChgHTML() {
914	let s = ''
915	s += '<label aria-label="Toggle theme">'
916	s += '<input type="checkbox" onchange="themeChg()" id="themeSlider">'
917	s += '<span id="themeSlider1" class="round"></span>'
918	s += '</label>'
919	return s
920}
921
922function themeChg() {
923	let was = themeGet()
924	let theme = was == 'lite' ? 'dark' : 'lite'
925	themeSet(theme)
926	document.getElementById('themeSlider').checked = theme == 'dark'
927}
928
929function themeGet() {
930	let theme = localStorage.getItem('theme')
931	if (theme !== 'lite' && theme !== 'dark') {
932		const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
933		theme = prefersDark ? 'dark' : 'lite'
934		localStorage.setItem('theme', theme)
935	}
936	return theme
937}
938
939function themeSet(theme) {
940	localStorage.setItem('theme', theme)
941	// console.log('themeSet', theme, document.documentElement.hasAttribute('theme'))
942	// if (document.documentElement.hasAttribute('theme')) {
943	// document.documentElement.removeAttribute('theme')
944	// } else {
945	document.documentElement.setAttribute('theme', theme)
946	// }
947}
948
949// function gAnalyticsStr() {
950//   let s = "";
951//   s += '<script type="text/javascript">';
952//   s += 'let _gaq = _gaq || [];';
953//   s += "_gaq.push(['_setAccount', 'UA-29771508-1']);";
954//   s += "_gaq.push(['_trackPageview']);";
955//   s += '(function() {';
956//   s += "let ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;";
957//   s += "ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';";
958//   s += "let s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);";
959//   s += '})();';
960//   s += '</script>';
961//   return s;
962// }
963
964function logErr() {
965	let pg = location.pathname
966	let pgHex = toHex(pg)
967	logSend(pgHex, 'err', window.location.hostname)
968}
969
970function logVisit() {
971	let pg = location.pathname
972	if (pg == '/') return // NB because getting too many root page visits for some reason.
973	let pgHex = toHex(pg)
974	logSend(pgHex, 'visit', window.location.hostname)
975}
976if (Math.random() <= 1) logVisit()
977
978function logSend(pg, viewtype, hostname) {
979	let req = new XMLHttpRequest()
980
981	let browser = (function (agent) {
982		switch (true) {
983			case agent.indexOf('edge') > -1:
984				return 'EdgeOld'
985			case agent.indexOf('edg') > -1:
986				return 'Edge'
987			case agent.indexOf('opr') > -1 && !!window.opr:
988				return 'Opera'
989			case agent.indexOf('chrome') > -1 && !!window.chrome:
990				return 'Chrome'
991			case agent.indexOf('trident') > -1:
992				return 'IE'
993			case agent.indexOf('firefox') > -1:
994				return 'Firefox'
995			case agent.indexOf('safari') > -1:
996				return 'Safari'
997			default:
998				return 'Other'
999		}
1000	})(window.navigator.userAgent.toLowerCase())
1001	let os = ''
1002	let tz = Intl.DateTimeFormat().resolvedOptions().timeZone
1003	let agent = navigator.userAgent.substring(0, 90)
1004
1005	let params = 'type=' + viewtype
1006	params += '&site=dlm'
1007	params += '&pg=' + encodeURIComponent(pg)
1008	params += '&lang=' + encodeURIComponent(window.navigator.language)
1009	params += '&ref=' + encodeURIComponent(toHex(document.referrer))
1010	params += '&wd=' + parseInt(window.innerWidth)
1011	params += '&ht=' + parseInt(window.innerHeight)
1012	params += '&ht=' + parseInt(window.innerHeight)
1013	params += '&browser=' + encodeURIComponent(browser)
1014	params += '&os=' + encodeURIComponent(os)
1015	params += '&tz=' + encodeURIComponent(tz)
1016	params += '&agent=' + encodeURIComponent(agent)
1017
1018	console.log('logSend', hostname)
1019
1020	req.open('POST', 'https://mi2f.com/update.php', true) // NB: false=synchronous
1021
1022	req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
1023	//req.setRequestHeader("Content-length", params.length);
1024	//req.setRequestHeader("Connection", "close");
1025	req.send(params)
1026}
1027
1028//window.addEventListener("scroll", onScroll);
1029function onScroll() {
1030	let scrollTop = document.documentElement ? document.documentElement.scrollTop : document.body.scrollTop
1031	let menu = document.getElementById('menu')
1032	//alert(scrollTop);
1033	if (scrollTop > 100) {
1034		menu.style.top = '0px'
1035		menu.style.width = '1000px'
1036		//menu.style.margin = "0 auto 0 auto";
1037		//menu.style.borderBottom = "2px solid #ffffff";
1038		menu.style.border = '1px solid #ffffff'
1039		menu.style.boxShadow = '0px 12px 12px -12px #77cc77'
1040		menu.style.backgroundColor = 'white'
1041		//menu.style.right = "0";
1042		//menu.style.left = "0";
1043		menu.style.textAlign = 'center'
1044		menu.style.position = 'fixed'
1045		menu.style.zIndex = '5000'
1046	} else {
1047		//menu.style.backgroundColor = "transparent";
1048		menu.style.boxShadow = 'none'
1049		menu.style.position = 'static'
1050	}
1051}
1052
1053function relatedLinks() {
1054	//this.id = typeof id !== 'undefined' ? id : "searchBox";
1055
1056	if (window.innerWidth < 860) {
1057		return // don't bother if screen not wide enough
1058	}
1059	let rels = document.getElementsByClassName('related')
1060	if (rels.length == 0) {
1061		return
1062	}
1063	let rel = rels[0]
1064
1065	let links = rel.getElementsByTagName('a')
1066	let count = links.length
1067
1068	let right = null
1069	let left = null
1070	if (count == 1) right = links[0]
1071	if (count > 1) {
1072		left = links[0]
1073		right = links[1]
1074	}
1075
1076	let s = ''
1077
1078	let top = 160
1079
1080	if (left != null) {
1081		s += '<div style="position: absolute; top: ' + top + 'px; left: 0px; text-align: left; z-index:2;">'
1082		s += fmtMenuBox(left.href, left.text, 0)
1083		s += '</div>'
1084	}
1085	if (right != null) {
1086		s += '<div style="position: absolute; top: ' + top + 'px; right: 0px; text-align: right; z-index:2;">'
1087		s += fmtMenuBox(right.href, right.text, 1)
1088		s += '</div>'
1089	}
1090
1091	document.getElementById('stt').innerHTML += s
1092	//document.getElementById("header").innerHTML += s;   // NB stops google translate from working
1093	//document.body.innerHTML = s + document.body.innerHTML;
1094	//document.getElementById("midfull").innerHTML += s;
1095}
1096
1097function fmtMenuBox(url, txt, dirn) {
1098	let s = ''
1099	let boxID = dirn == 0 ? 'menuLt' : 'menuRt'
1100	s += '<a href="' + url + '"  style="text-decoration: none; color: #8ac; line-height:1.3rem; ">'
1101	s += '<div id="' + boxID + '">'
1102	s += txt
1103	s += '</div>'
1104	s += '</a>'
1105	return s
1106}
1107
1108function visFlip(id) {
1109	let div = document.getElementById(id)
1110	console.log('visFlip', id, div.style.visibility)
1111	if (div.style.visibility === 'hidden') {
1112		div.style.visibility = 'visible'
1113	} else {
1114		div.style.visibility = 'hidden'
1115	}
1116}
1117
1118function vidDoLiteral(id, spanid) {
1119	let visIndex = videoVis.indexOf(spanid)
1120	let visQ = visIndex > -1 ? true : false
1121
1122	if (visQ) {
1123		// Turn off
1124		let frame = document.getElementById(spanid + 'v1')
1125		if (frame) frame.parentNode.removeChild(frame)
1126		videoVis.splice(visIndex, 1)
1127		visQ = false
1128
1129		// Optional: Clear the container content so the border disappears?
1130		// document.getElementById(spanid).innerHTML = '';
1131	} else {
1132		// Turn on
1133		// Using Template Literal (Backticks) for cleaner HTML
1134		let s = `
1135            <div style="width:100%; clear:both;"></div>
1136            <iframe id="${spanid}v1" 
1137                src="https://www.youtube-nocookie.com/embed/${id}?rel=0&autoplay=1&modestbranding=1" 
1138                frameborder="0" 
1139                allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 
1140                allowfullscreen 
1141                style="vertical-align:top;">
1142            </iframe>
1143            <div style="width:100%;"></div>
1144        `
1145
1146		let vid = document.getElementById(spanid)
1147		vid.innerHTML = s
1148		//vid.style.border = '5px solid blue;' // Keeping your test border for now
1149
1150		videoVis.push(spanid)
1151		visQ = true
1152
1153		// Resize immediately
1154		resizeVideo(spanid)
1155	}
1156}
1157
1158function vidDo(id, spanid) {
1159	let s = ''
1160
1161	let visIndex = videoVis.indexOf(spanid)
1162	let visQ = visIndex > -1 ? true : false // is it currently visible?
1163
1164	if (visQ) {
1165		// turn off
1166		let frame = document.getElementById(spanid + 'v1')
1167		if (frame) frame.parentNode.removeChild(frame)
1168		videoVis.splice(visIndex, 1)
1169		visQ = false
1170	} else {
1171		// turn on
1172		s += '<div style="clear:both;"></div>'
1173
1174		s += `<iframe id="${spanid}v1" 
1175		src="https://www.youtube-nocookie.com/embed/${id}?rel=0&autoplay=1" 
1176		frameborder="0" 
1177		allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
1178		allowfullscreen 
1179		style="vertical-align:top;"></iframe>`
1180
1181		s += '<div style="clear:both;""></div>'
1182		videoVis.push(spanid)
1183		visQ = true
1184		console.log('vidDo', s)
1185	}
1186
1187	let vid = document.getElementById(spanid)
1188	vid.innerHTML = s
1189
1190	// we can likely remove the border now, but keep for testing:
1191	//vid.style.border = '5px solid blue;'
1192	console.log('vidDo', vid)
1193
1194	if (visQ) vidResize(spanid)
1195}
1196
1197function vidResize(spanid) {
1198	let v1 = document.getElementById(spanid + 'v1')
1199	if (!v1) return
1200
1201	// let wd = window.innerWidth - 40
1202	let contentBox = document.getElementById('content')
1203	// We subtract a small buffer (e.g. 5px) just to prevent rounding errors causing scrollbars
1204	let wd = contentBox ? contentBox.offsetWidth - 5 : window.innerWidth - 40
1205	if (wd > 800) wd = 800
1206	console.log('vidResize', wd)
1207
1208	v1.style.width = wd + 'px'
1209	// v1.style.height = wd * (340 / 640) + 80 + 'px'
1210	v1.style.height = (wd * 9) / 16 + 'px'
1211}
1212
1213let videoVis = []
1214
1215function mainDo() {
1216	// Dynamically add hreflang tag to the head section
1217	// <link rel="alternate" hreflang="en" href="..." />
1218	let hreflangLink = document.createElement('link')
1219	hreflangLink.rel = 'alternate'
1220	hreflangLink.hreflang = 'en'
1221	hreflangLink.href = window.location.href
1222	document.head.appendChild(hreflangLink)
1223
1224	if (document.getElementById('menuWide') == null) return
1225
1226	document.getElementById('menuWide').innerHTML = menuHTML(0)
1227	document.getElementById('menuSlim').innerHTML = menuHTML(1)
1228	document.getElementById('menuTiny').innerHTML = menuHTML(2)
1229
1230	document.getElementById('adHide').innerHTML = adsHideHTML()
1231	if (localStorage.getItem('cookie') != 'ok') {
1232		document.getElementById('cookOK').innerHTML = cookMsg()
1233	}
1234
1235	document.getElementById('search').innerHTML = searchHTML()
1236	document.getElementById('linkto').innerHTML = linksHTML()
1237
1238	let themeDiv = document.getElementById('themeChg')
1239	if (themeDiv) {
1240		themeDiv.innerHTML = themeChgHTML()
1241	} else {
1242		let newDiv = document.createElement('div')
1243		newDiv.id = 'themeChg'
1244
1245		let logoDiv = document.getElementById('logo')
1246		if (logoDiv) {
1247			console.log('ADD theme div')
1248			logoDiv.insertAdjacentElement('afterend', newDiv)
1249			newDiv.innerHTML = themeChgHTML()
1250		}
1251	}
1252
1253	document.getElementById('themeSlider').checked = themeGet() == 'dark' // must be on page before using
1254
1255	document.getElementById('footer').innerHTML = footerHTML()
1256
1257	let qDivs = document.querySelectorAll('.questions')
1258	//console.log('qDivs', qDivs)
1259	for (let i = 0; i < qDivs.length; i++) {
1260		qsExpand(qDivs[i])
1261	}
1262
1263	let vidDivs = document.querySelectorAll('.video')
1264	//console.log('vidDivs', vidDivs)
1265	for (let i = 0; i < vidDivs.length; i++) {
1266		vidExpand(vidDivs[i], i)
1267	}
1268
1269	let scriptDivs = document.querySelectorAll('.script')
1270	//console.log('scriptDivs', scriptDivs)
1271	for (let i = 0; i < scriptDivs.length; i++) {
1272		scriptExpand(scriptDivs[i])
1273	}
1274
1275	adsHideSet()
1276
1277	if (!adOffQ()) {
1278		setTimeout(adsDo, 2000)
1279	}
1280
1281	doLocal()
1282	eqFmts()
1283	document.getElementById('searchFld').addEventListener('keyup', searchKey)
1284
1285	main.sock = new MainSock()
1286	if (typeof mainMsg === 'function') mainMsg({ msg: 'ready' })
1287
1288	loginPrompt()
1289	console.log('loginName', loginName())
1290}
1291
1292function mainVar() {
1293	return main
1294}
1295
1296window.addEventListener('message', function (event) {
1297	if (event.origin !== window.location.origin) return
1298	// console.log('>> main4 rcvd', event.data)
1299
1300	if (event.data && event.data.msg) {
1301		let msg = event.data.msg
1302
1303		// if (msg == 'gamesList') {
1304		main.sock.send(JSON.stringify(event.data))
1305		// }
1306	}
1307
1308	// if (event.data === 'iframe-ready') {
1309	// 	let iframe = event.source
1310	// 	console.log('******* iframe',event)
1311	// 	// iframe.postMessage('main-recvd', '*')
1312	// 	main.iframes.push(iframe)
1313	// }
1314})
1315
1316function searchHTML() {
1317	let s = ''
1318	s += '<form autocomplete="off" action="' + urlSttGet() + 'search/search.html" method="get">'
1319	s += '<input type="submit" id="searchBtn" name="submit" value="" aria-label="Search Button" />'
1320	s += '<input type="text" id="searchFld" name="query" value="" placeholder="Search" aria-label="Search" />'
1321	s += '<input type="hidden" name="search" value="1" />'
1322	s += '</form>'
1323
1324	return s
1325}
1326
1327function searchKey(ev) {
1328	let val = ev.target.value
1329	console.log('searchKey', val)
1330
1331	// run external script to handle autocomplete etc (better than adding heaps of code and possible errors to main script )
1332	if (main.searchScript == 'no') {
1333		main.searchScript = 'loading'
1334		let script = document.createElement('script')
1335		script.onload = function () {
1336			console.log('Script loaded')
1337			searchDo(ev)
1338			main.searchScript = 'yes'
1339		}
1340		let imgHome = location.hostname == 'localhost' ? `/${main.dom}/` : '/'
1341		script.src = imgHome + 'search/images/search-lib.js'
1342		document.getElementsByTagName('head')[0].appendChild(script)
1343	}
1344	if (main.searchScript == 'yes') searchDo(ev)
1345	return
1346}
1347
1348function qsExpand(node) {
1349	let str = node.innerHTML
1350	if (str.includes('getQ(')) return // don't change an existing 'inline' getQ() style
1351
1352	console.log('qsExpand', str)
1353	let newStr = qsHTML(str)
1354	node.innerHTML = newStr
1355}
1356
1357function qsHTML(qStr) {
1358	let qs = qStr.trim().split(/[^0-9]+/) //   use '+' (1 to many) of anything that is not a number as a separator
1359	let joinedStr = qs.join('_')
1360	let s = ''
1361
1362	s += '<span style="display:inline-block; min-width:8rem;">Mathopolis:</span>'
1363	for (let i = 0; i < qs.length; i++) {
1364		s += '<a href="javascript:doQ(' + qs[i] + ",'" + joinedStr + "'" + ')">Q' + (i + 1) + '</a> '
1365	}
1366
1367	let path = location.pathname
1368	path = path.split(`/${main.dom}/`).join('/') // for local
1369	let dir = path.substring(0, path.lastIndexOf('/'))
1370	console.log('dir', path, dir)
1371
1372	// if (dir == '/data') {
1373	// 	s += '<br>'
1374	// 	s += '<span style="display:inline-block; min-width:8rem;">Local popup:</span>'
1375	// 	for (let i = 0; i < qs.length; i++) {
1376	// 		s += '<a href="javascript:doQLocal(' + qs[i] + ",'" + joinedStr + "'" + ')">Q' + (i + 1) + '</a> '
1377	// 	}
1378	// }
1379
1380	return s
1381}
1382
1383function vidExpand(div, n) {
1384	let str = div.innerHTML
1385	if (str.trim().length <= 1) return // don't change an empty (old style) video tag
1386
1387	let newStr = vidHTML(str, 'title', 'video' + n)
1388	div.innerHTML = newStr
1389}
1390
1391function vidHTML(id, titleid = 'title', spanid = 'video', style = 'span') {
1392	// it is possible that a '-' gets converted to '&minus;' in html, so revert
1393	id = id.replace(/&minus;/g, '-')
1394
1395	console.log('vidHTML', id)
1396
1397	let s = ''
1398	switch (style) {
1399		case 'span':
1400			//s += '<div class="video">';
1401			s += '<a href="javascript:vidDo(\'' + id + "','" + spanid + '\')">'
1402			s += '<img src="' + urlSttGet() + 'images/style/video.svg" alt="Video" style="width:56px; height:49px;vertical-align:middle; border:none;" />'
1403			s += '</a>'
1404			//s += '</div>';
1405			s += '<span id="' + spanid + '" style=""></span>'
1406			break
1407
1408		case 'h1':
1409			let title = document.getElementById(titleid).innerHTML
1410
1411			s += '<div class="centerfull" style="clear:both; font-weight:400; padding: 0;">'
1412			s += '<div style="float:left; width:60px; text-align:left;">'
1413			s += '  <a href="javascript:vidDo(\'' + id + "','" + spanid + '\')">'
1414			s += '    <img src="' + urlSttGet() + 'images/style/video.svg" alt="Video" height="32" style="vertical-align:middle; border:none;" />'
1415			s += ' </a>'
1416			s += '</div>'
1417			s += '<div style="float:right; width:60px; text-align:right;">&nbsp;</div>'
1418			s += '  <div style="margin:0 auto;">'
1419			s += '    <h1>' + title + '</h1>'
1420			s += '  </div>'
1421			s += '</div>'
1422			break
1423
1424		case 'h2':
1425			title = document.getElementById(titleid).innerHTML
1426
1427			s += '<div style="float:right; width:100px; margin: -10px 0 5px 0;">'
1428			s += '  <a href="javascript:vidDo(\'' + id + "','" + spanid + '\')">'
1429			s += '    <img src="' + urlSttGet() + 'images/style/video.svg" alt="Video" height="32" style="vertical-align:middle; border:none;" />'
1430			s += ' </a>'
1431			s += '</div>'
1432			s += '    <h2>' + title + '</h2>'
1433			break
1434	}
1435	console.log('vidHTML', s)
1436	return s
1437}
1438
1439function initVideo(id, titleid, spanid, style) {
1440	// defaults
1441	titleid = typeof titleid !== 'undefined' ? titleid : 'title'
1442	spanid = typeof spanid !== 'undefined' ? spanid : 'video'
1443	style = typeof style !== 'undefined' ? style : 'h1'
1444
1445	document.getElementById(titleid).innerHTML = vidHTML(id, titleid, spanid, style)
1446}
1447
1448function scriptExpand(div) {
1449	let str = div.innerHTML
1450	if (str.trim().length <= 1) return
1451
1452	iframeDo(div, str)
1453}
1454
1455function htmlSafe(input) {
1456	// Return empty string for undefined or empty input
1457	if (input == undefined || input.length <= 0) return ''
1458
1459	let safeInput = input.replace(/javascript/gi, 'js')
1460	safeInput = safeInput.replace(/script/gi, '')
1461
1462	// Strip out potentially harmful characters
1463	safeInput = safeInput.replace(/[^a-z0-9/\-[\]=_ :;,.()\w\s!?]/gi, '')
1464
1465	return safeInput
1466}
1467function sqlSafe(sql) {
1468	let s = ''
1469	const length = sql.length
1470
1471	for (let i = 0; i < length; i++) {
1472		const char = sql[i]
1473		if (char === "'") {
1474			s += "''"
1475		} else if (char === '"') {
1476			s += '\\"'
1477		} else if (char === '\\') {
1478			s += '\\\\'
1479		} else {
1480			s += char
1481		}
1482	}
1483
1484	return s
1485}
1486
1487function putFlash6(w, h, fn, querystring, clr) {
1488	// w=width, h=height, fn=filename(minus '.swf'), querystring=..., clr=bg color
1489	if (!querystring) {
1490		querystring = ''
1491	} else {
1492		querystring = '?' + querystring
1493	}
1494
1495	if (!clr) {
1496		clr = '#d6d9e6'
1497	} // default background color
1498
1499	if (fn.substring(fn.lastIndexOf('.swf')) != '.swf') fn = fn + '.swf' // append .swf if absent
1500
1501	var s = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/s
1501hockwave/cabs/flash/swflash.cab#version=6,0,79,0" width="' + w + '" height="' + h + '" id="' + fn + '">\n'
1502	s += '<param name="movie" value="' + fn + querystring + '"/> '
1503	s += '<param name="quality" value="high"/> '
1504	s += '<param name="bgcolor" value="' + clr + '"/> '
1505	s += '<param name="menu" value="false"/> '
1506	s += '<param name="allowScriptAccess" value="sameDomain"/> ' //        always
1507	s += '<param name="allowFullScreen" value="true"/> '
1508	s += '<embed src="' + fn + querystring + '" quality="high" bgcolor="' + clr + '" '
1509	s += 'menu="false" width="' + w + '" height="' + h + '" type="application/x-shockwave-flash" '
1510	s += 'pluginspage="http://www.macromedia.com/go/getflashplayer" '
1511	s += 'swLiveConnect="true" allowscriptaccess="sameDomain" allowFullScreen="true" id="' + fn + '" name="' + fn + '"><noembed></noembed></embed>\n' //       always
1512	s += '</object>\n'
1513
1514	document.write(s)
1515}
1516
1517function putFlash7(w, h, swf, querystring, clr, noflash) {
1518	// https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#web
1519	console.log('putFlash7: ' + swf)
1520
1521	let s = '<div id="container"></div>'
1522	document.write(s)
1523
1524	window.RufflePlayer = window.RufflePlayer || {}
1525	window.addEventListener('load', (event) => {
1526		const ruffle = window.RufflePlayer.newest()
1527		const player = ruffle.createPlayer()
1528		const container = document.getElementById('container')
1529		container.appendChild(player)
1530		player.load(swf)
1531	})
1532}
1533
1534function putFlash8(w, h, swf, querystring, clr, noflash) {
1535	console.log('putFlash8: ' + swf)
1536
1537	window.RufflePlayer = window.RufflePlayer || {}
1538	window.addEventListener('load', (event) => {
1539		const ruffle = window.RufflePlayer.newest()
1540		const player = ruffle.createPlayer()
1541		const container = document.getElementById('container')
1542		console.log('container', container)
1543		container.appendChild(player)
1544		player.load(swf)
1545	})
1546}
1547
1548function iframeDo(div, fileStr, focusQ = true) {
1549	// create an iframe and then load filename into it
1550	let isPreview = div.classList.contains('preview') // Check if we should start small
1551	let previewHtVal = getComputedStyle(div).getPropertyValue('--preview-ht') || '72px'
1552	let previewHt = parseInt(previewHtVal)
1553
1554	let fileNames = fileStr.split(',')
1555	let filename = fileNames[fileNames.length - 1].trim() // last file name is our main app
1556
1557	let imgHome = location.hostname == 'localhost' ? `/${main.dom}/` : '/'
1558
1559	let sttWd = parseInt(div.getBoundingClientRect().width)
1560	let sttHt = parseInt(div.getBoundingClientRect().height)
1561
1562	let aspect = parseFloat(div.style.gap)
1563	let maxQ = !isNaN(aspect)
1564	console.log('iframeDo:', filename, div)
1565
1566	let htQ = div.style.height.length > 0
1567
1568	let iframe = document.createElement('iframe')
1569	//let maxQ = false
1570	if (htQ) {
1571		//if (sttHt>=1000) maxQ = true
1572		iframe.width = sttWd
1573		iframe.style.width = iframe.width + 'px'
1574
1575		// iframe.height = isPreview ? 91 : sttHt // Toggle initial height
1576		iframe.height = isPreview ? previewHt : sttHt
1577		iframe.style.height = iframe.height + 'px'
1578	} else {
1579		iframe.height = 200 // speculative height, adjust later
1580		iframe.style.height = iframe.height + 'px'
1581	}
1582
1583	iframe.style = `display:block; margin:auto; border:none; background-color:transparent; overflow:hidden;
1584  transition:all 0.6s ease-in-out;`
1585
1586	if (isPreview) iframe.style.pointerEvents = 'none' // Don't let iframe steal click
1587
1588	iframe.scrolling = 'no' // sadly needed for chrome
1589	iframe.setAttribute('title', 'JavaScript Animation')
1590
1591	let html = `<!doctype html><html theme="${themeGet()}">
1592	<head>
1593	<meta http-equiv="content-type" content="text/html; charset=utf-8" />`
1594
1595	// supporting files
1596	for (let i = 0; i < fileNames.length - 1; i++) {
1597		html += '<script src="' + imgHome + fileNames[i].trim() + '"></script>'
1598	}
1599
1600	html += '<link rel="stylesheet" href="' + imgHome + 'style4.css">'
1601	html += '<script src="' + imgHome + 'iframe.js" defer></script>'
1602	html += '<style>body { background: transparent; }</style>' // don't want to double up on background in iframe
1603	html += '</head>'
1604
1605	html += '<body style="margin:auto; text-align:center; ">'
1606	html += `<script defer src="${filename}"></script>`
1607	html += '</body>'
1608	html += '</html>'
1609
1610// console.log('html',html)
1611
1612	// Handle the click-to-expand overlay
1613	let targetElement = iframe
1614
1615	if (isPreview) {
1616		let container = document.createElement('div')
1617		container.className = 'script-container preview' // Add classes here
1618
1619		let overlay = document.createElement('div')
1620		overlay.className = 'script-overlay' // Add class here
1621
1622		overlay.height = previewHt
1623		overlay.style.height = iframe.height + 'px'
1624
1625		// overlay.innerHTML = '<span>Click to Interact</span>'
1626
1627		container.onclick = function () {
1628			isPreview = false
1629			iframe.height = sttHt
1630			iframe.style.height = iframe.height + 'px'
1631
1632			iframe.style.pointerEvents = 'auto'
1633			container.classList.remove('preview') // Clean up classes
1634			overlay.remove()
1635		}
1636
1637		container.appendChild(iframe)
1638		container.appendChild(overlay)
1639		targetElement = container
1640	}
1641
1642	div.parentElement.insertBefore(targetElement, div) // place iframe (or container)
1643	div.style.display = 'none' // hide 'calling' div
1644
1645	iframe.contentWindow.document.open()
1646	iframe.contentWindow.document.write(html)
1647	// iframe.contentWindow.document.outerHTML=html   // doesn't work
1648	iframe.contentWindow.document.close()
1649
1650	iframe.addEventListener('load', function () {
1651		main.iframes.push(iframe.contentWindow)
1652
1653		if (focusQ && !isPreview) {
1654			iframe.contentWindow.focus()
1655		}
1656
1657		let btnQ = false
1658		let aQ = false
1659
1660		iframe.style.maxWidth = '100%'
1661		//let bestWd = iframe.contentDocument.body.scrollWidth
1662		let bestWd = 0,
1663			bestHt = 0
1664		if (htQ) {
1665			bestWd = sttWd
1666			bestHt = isPreview ? previewHt : sttHt
1667		} else {
1668			bestWd = 0
1669			iframe.width = bestWd
1670			iframe.style.width = iframe.width + 'px'
1671
1672			bestHt = iframe.contentDocument.body.scrollHeight + 1
1673			if (!isPreview) {
1674				iframe.height = bestHt
1675				iframe.style.height = iframe.height + 'px'
1676			}
1677		}
1678
1679		setInterval(function () {
1680			if (isPreview) return // Stay at previewHt until clicked
1681
1682			if (maxQ) {
1683				let wd = window.innerWidth
1684				let ht = window.innerHeight - 60
1685				let newAspect = wd / ht
1686
1687				//iframe.style.border = '1px solid blue'
1688
1689				// console.log('aspect',aspect,newAspect)
1690				if (newAspect > aspect) {
1691					wd = ht * aspect
1692				} else {
1693					ht = wd / aspect
1694				}
1695				//console.log('maxQ',screen.height,screen.availHeight,window.innerHeight,aspect,wd,ht)
1696				iframe.width = wd
1697				iframe.style.width = iframe.width + 'px'
1698
1699				iframe.height = ht
1700				iframe.style.height = iframe.height + 'px'
1701
1702				return
1703			}
1704			let wd, ht
1705			let byBoundingRect = false
1706			if (byBoundingRect) {
1707				let div = iframe.contentDocument.body.children[0].children[0]
1708				let rect = div.getBoundingClientRect()
1709				wd = rect.width
1710				ht = rect.height
1711			} else {
1712				wd = iframe.contentDocument.body.scrollWidth + 1
1713				ht = iframe.contentDocument.body.scrollHeight + 1
1714			}
1715
1716			if (wd != bestWd) {
1717				console.log('chg wd', bestWd, 'to', wd)
1718				bestWd = wd
1719				// iframe.style.width = bestWd + 'px';
1720				if (!aQ) {
1721					iframe.width = '100%'
1722					iframe.style.width = '100%'
1723				} else {
1724					iframe.width = bestWd
1725					iframe.style.width = bestWd + 'px'
1726					aQ = true
1727				}
1728			}
1729
1730			// console.log('ht', ht, bestHt)
1731			if (ht > bestHt || ht < bestHt - 10) {
1732				console.log('chg ht', bestHt, 'to', ht)
1733				bestHt = ht
1734				iframe.height = bestHt
1735				iframe.style.height = iframe.height + 'px'
1736			}
1737			if (!btnQ) {
1738				let wd = iframe.contentDocument.body.scrollWidth + 40
1739				let ht = iframe.contentDocument.body.scrollHeight + 1
1740				let div = iframe.contentDocument.getElementById('resize')
1741				if (div != null) {
1742					let btnStr = '<button type="button" class="btn" style="z-index:2; position:absolute; right:0; bottom:0;" onclick="iframeMax(\'' + filename + "'," + wd + ',' + ht + ')">&rect;</button>'
1743					div.innerHTML = btnStr
1744					//console.log('qqq', wd, ht, btnStr)
1745				}
1746				//iframe.contentDocument.body.innerHTML += btnStr
1747				btnQ = true
1748			}
1749		}, 650)
1750	})
1751}
1752
1753function rpLog(id) {
1754	let now = performance.now()
1755	if (main.timeStt == undefined) {
1756		main.timeStt = performance.now()
1757		main.timePrev = performance.now()
1758	}
1759
1760	console.log('' + id + ': ' + parseInt(now - main.timeStt) + 'ms => ' + parseInt(now - main.timePrev) + 'ms')
1761	main.timePrev = now
1762}
1763
1764function loginShow(type) {
1765	let s = ''
1766
1767	if (type == 0) {
1768		// not logged in
1769		let name = ''
1770		let pass = ''
1771		s += '<div style="position:relative; margin:auto; text-align:center; font:14px Arial;  margin: 5px 0 1px 0; padding: 5px 0 5px 0;">'
1772		s += 'Username: '
1773		s += '<input id="name" style="width: 120px; border-radius: 10px; font: 18px Arial; background-color: #eeffee; text-align:center;" value="' + name + '" />'
1774		s += '<br>'
1775		s += 'Password: '
1776		s += '<input id="pass" type="password" style="width: 120px; border-radius: 10px; font: 18px Arial; background-color: #eeffee; text-align:center;" value="' + pass + '" />'
1777		s += '</div>'
1778
1779		s += '<div style="margin: 0 0 5px 10px;">'
1780		s += '<button id="optLogin" onclick="loginGo()" style="z-index:2;" class="btn" >Login</button>'
1781		s += '<button id="optNewUser" onclick="loginNew()" style="z-index:2;" class="btn" >New User</button>'
1782		s += '</div>'
1783	}
1784
1785	if (type == 1) {
1786		// logged in
1787		s += `
1788	Hi ${loginName()}<br>
1789	<div style="position:relative; margin:auto; text-align:right; font:14px Arial;  margin: 5px 0 1px 0; padding: 5px 0 5px 0;">
1790		Password: <input id="passOld" type="password" style="width: 120px; border-radius: 10px; font: 18px Arial; background-color: #eeffee; text-align:center;" value="" />
1791		<br>
1792		New Password: <input id="passNew" type="password" style="width: 120px; border-radius: 10px; font: 18px Arial; background-color: #eeffee; text-align:center;" value="" />
1793		<button onclick="loginPassChg()" style="z-index:2;" class="btn" >
1793Change Password</button>
1794	</div>
1795		<br>
1796		<button onclick="userGames()" style="z-index:2;" class="btn" >Games</button><br>
1797		<button onclick="loginOut()" style="z-index:2;" class="btn" >Logout</button>
1798	`
1799	}
1800
1801	document.getElementById('loginContent').innerHTML = s
1802
1803	let div = document.getElementById('loginPop')
1804	div.style.left = '-250px'
1805	div.style.opacity = 1
1806
1807	document.getElementById('name').value = localStorage.getItem('user.name')
1808}
1809
1810function loginName() {
1811	let name = localStorage.getItem('user.name') || ''
1812	if (name.length < 2) return ''
1813
1814	let stamp = localStorage.getItem('user.stamp') || ''
1815	if (stamp.length < 2) return ''
1816
1817	return name
1818}
1819
1820function loginType() {
1821	let type = 'none' // or 'guest'?
1822
1823	let name = localStorage.getItem('user.name') || ''
1824	if (name.length < 2) return type
1825
1826	let userType = localStorage.getItem('user.type') || type
1827	if (userType == 1 || userType == 2) return 'admin'
1828
1829	return 'user'
1830}
1831
1832function loginPrompt() {
1833	// return
1834	let name = loginName()
1835
1836	let s = ''
1837	if (name.length > 0) {
1838		s += '<a href="javascript:loginShow(1)" style="aria-label="Logout">'
1839		s += name
1840		s += '</a>'
1841	} else {
1842		s += '<a href="javascript:loginShow(0)" style="aria-label="Login">'
1843		s += 'Login'
1844		s += '</a>'
1845	}
1846
1847	let div = document.getElementById('login')
1848	div.innerHTML = s
1849}
1850
1851function loginGo() {
1852	let name = document.getElementById('name').value
1853	localStorage.setItem('user.name', name)
1854	let pass = document.getElementById('pass').value
1855	console.log('loginGo', name, pass)
1856	loginMsg()
1857
1858	main.sock.login(name, pass)
1859}
1860function loginNew() {
1861	let name = document.getElementById('name').value
1862	localStorage.setItem('user.name', name) // remember for user later on
1863	let pass = document.getElementById('pass').value
1864	console.log('loginNew', name, pass) // don't store password at all
1865	loginMsg()
1866
1867	// validate
1868	if (name.length < 6) {
1869		loginMsg('Username needs 6 or more characters')
1870		return
1871	}
1872	if (name.length > 20) {
1873		loginMsg('Username exceeds maxiumum of 20 characters')
1874		return
1875	}
1876	let safeRegex = /^[a-zA-Z0-9_\-!$]+$/
1877	if (!safeRegex.test(name)) {
1878		loginMsg('Username has unsafe characters')
1879		return
1880	}
1881	if (pass.length < 6) {
1882		loginMsg('Password needs 6 or more characters')
1883		return
1884	}
1885	if (!safeRegex.test(pass)) {
1886		loginMsg('Password: has unsafe characters')
1887		return
1888	}
1889
1890	main.sock.loginNew(name, pass)
1891}
1892function loginPassChg() {
1893	let name = loginName()
1894	let passOld = document.getElementById('passOld').value
1895	let passNew = document.getElementById('passNew').value
1896	console.log('loginPassChg', passOld, passNew)
1897
1898	// validate
1899	if (passNew.length < 6) {
1900		loginMsg('Password: 6 or more characters')
1901		return
1902	}
1903
1904	main.sock.loginPassChg(name, passOld, passNew)
1905}
1906function loginOut() {
1907	localStorage.setItem('user.stamp', '') // unsetting stamp invalidates login
1908	loginPrompt()
1909	loginHide()
1910	if (typeof loginDo === 'function') loginDo()
1911}
1912function loginMsg(s = '&nbsp;') {
1913	console.log('loginMsg', s)
1914	let div = document.getElementById('loginMsg')
1915	if (div != null) div.innerHTML = s
1916}
1917
1918function loginHide() {
1919	let div = document.getElementById('loginPop')
1920	div.style.left = '200px'
1921	div.style.opacity = 0
1922}
1923function userGames() {
1924	let home = location.hostname == 'localhost' ? `/${main.dom}/` : '/'
1925	window.location = home + 'games/user.html'
1926}
1927
1928function fmtTm(dt) {
1929	let hours = dt.getHours().toString().padStart(2, '0')
1930	let minutes = dt.getMinutes().toString().padStart(2, '0')
1931	let seconds = dt.getSeconds().toString().padStart(2, '0')
1932	return `${hours}:${minutes}:${seconds}`
1933}
1934
1935function getSock() {
1936	// this is how we get the main socket when in an iframe
1937	return main.sock
1938}
1939
1940class MainSock {
1941	constructor(keepAliveQ = false) {
1942		this.keepAliveQ = keepAliveQ
1943		this.socket = null
1944		this.queue = []
1945		this.lines = []
1946		this.connectTryN = 0
1947	}
1948
1949	send(msg) {
1950		// 0=CONNECTING (Socket created, connection not yet open) 1=OPEN (connection is open and ready to communicate)  2=CLOSING (connection is in the process of closing)  3=CLOSED
1951
1952		if (!this.socket || this.socket.readyState === WebSocket.CLOSED) {
1953			this.connect()
1954		}
1955
1956		if (this.socket.readyState === WebSocket.OPEN) {
1957			this.socket.send(msg)
1958		} else {
1959			// If the WebSocket is not OPEN, queue the message
1960			this.queue.push(msg)
1961		}
1962	}
1963
1964	send1(msg) {
1965		if (this.socket == null) {
1966			// console.log('this.queue.push',msg)
1967			this.queue.push(msg)
1968			this.connect()
1969		} else {
1970			console.log('this.socket.readyState', this.socket.readyState)
1971			if (this.socket.readyState === 1) {
1972				// console.log('this.socket.send',msg)
1973				this.socket.send(msg)
1974			}
1975		}
1976		//this.waitForConnection(function () { this.socket.send(msg); }, 1000)
1977	}
1978
1979	waitForConnection(callback, interval) {
1980		if (this.socket.readyState === 1) {
1981			callback()
1982		} else {
1983			let self = this
1984			interval *= 2
1985			setTimeout(function () {
1986				self.waitForConnection(callback, interval)
1987			}, interval)
1988		}
1989	}
1990	connect() {
1991		// console.trace('connect', fmtTm(new Date()))
1992
1993		let socket = new WebSocket(`wss://${main.server}:${main.port}/`)
1994		console.log('socket', socket)
1995		let self = this
1996		socket.onopen = function (ev) {
1997			// console.log('[open] Connection established')
1998			self.connectTryN = 0
1999			// console.log('Sending to server')
2000
2001			// Empty the queue by sending queued messages.
2002			while (self.queue.length > 0) {
2003				let msg = self.queue.shift()
2004				// console.log('msg from queue:', msg)
2005				socket.send(msg)
2006			}
2007
2008			// Make sure we only add this listener once after a successful connection.
2009			window.addEventListener(
2010				'beforeunload',
2011				() => {
2012					console.log('Browser window is closing, closing WebSocket.')
2013					socket.close(1000, 'Window closing') // Use standard WebSocket close code 1000 for normal closure.
2014				},
2015				{ once: true },
2016			)
2017		}
2018		socket.onclose = function (ev) {
2019			console.log(ev.wasClean ? 'Disconnected' : 'Connection break: ' + ev.code + ' ' + ev.reason)
2020			// if (!ev.wasClean ) {
2021			// 	if (this.keepAliveQ) self.attemptReconnect()
2022			// }
2023		}
2024		socket.onmessage = function (ev) {
2025			//console.log('RECV:', ev.data)
2026			let data
2027			try {
2028				data = JSON.parse(ev.data)
2029			} catch (err) {
2030				data = {}
2031				//console.log('socket message error:', err);
2032			}
2033			self.msgRcv(data)
2034		}
2035		socket.onerror = function (err) {
2036			console.log('Socket Error ' + err.message)
2037			if (self.keepAliveQ) self.attemptReconnect()
2038
2039			// either it has a mainMsg function or is an iframe that we need to postMessage to
2040			if (typeof mainMsg === 'function') mainMsg({ msg: 'nosocket' })
2041			main.iframes.forEach((iframe) => {
2042				iframe.postMessage({ msg: 'nosocket' }, '*')
2043			})
2044		}
2045		this.socket = socket
2046		// console.log('socket running:' + this.socket)
2047	}
2048
2049	attemptReconnect() {
2050		this.connectTryN++
2051
2052		// Delay times for each reconnect attempt.
2053		let delays = [
2054			{ tm: 1, txt: '', why: 'unused' },
2055			{ tm: 5, txt: '', why: 'net glitch' },
2056			{ tm: 30, txt: '', why: 'server restart' },
2057			{ tm: 120, txt: '', why: 'bad server restart' },
2058			{ tm: 600, txt: '', why: 'one last try' },
2059		]
2060
2061		if (this.connectTryN < delays.length) {
2062			let delay = delays[this.connectTryN].tm
2063			console.log(`retry connection ${this.connectTryN} at ${fmtTm(new Date())} after ${delay}s`)
2064			setTimeout(() => this.connect(), delay * 1000)
2065		} else {
2066			// Handle the case when all reconnect attempts fail.
2067			// For example, show a reconnect failure message to the user.
2068		}
2069	}
2070
2071	msgRcv(data) {
2072		//console.log('msgRcv', JSON.stringify(data).substring(0, 200))
2073		// this.histAdd('msgRcv:' + JSON.stringify(data))
2074		switch (data.msg) {
2075			case 'usergood': {
2076				this.histAdd('Login success!')
2077				this.userSet('stamp', data.stamp)
2078				this.userSet('stampWhen', new Date())
2079
2080				let row = data
2081				let props = { grp: row.grp, usertype: row.usertype, regdate: row.regdate, lastdate: row.lastdate, merits: row.merits, qCount: row.qCount, achieveCount: row.achieveCount, qScore: row.qScore, lastQs: row.lastQs, failQs: row.failQs, mentor: row.mentor }
2082				console.log('row', row)
2083				for (let key in row) {
2084					if (row.hasOwnProperty(key)) {
2085						// Check if the property is directly on the row object
2086						this.userSet(key, row[key])
2087					}
2088				}
2089				loginPrompt()
2090				loginHide()
2091				break
2092			}
2093
2094			case 'usernew': {
2095				this.histAdd('New User success!')
2096				this.userSet('stamp', data.stamp)
2097
2098				let date = new Date()
2099				let now = date.toISOString()
2100				let props = { grp: '', regdate: now, lastdate: now, merits: 0, qCount: 0, achieveCount: 0, qScore: 0, lastQs: '', failQs: '', mentor: '' }
2101				console.log('props', props)
2102				for (let key in props) {
2103					if (props.hasOwnProperty(key)) {
2104						this.userSet(key, props[key])
2105					}
2106				}
2107				loginPrompt()
2108				loginHide()
2109				break
2110			}
2111
2112			case 'userchg':
2113				this.userSet('stamp', data.stamp)
2114				loginMsg('Change successful!')
2115				//loginHide()
2116				break
2117
2118			case 'usertaken':
2119				loginMsg('User taken')
2120				this.userSet('stamp', '')
2121				break
2122
2123			case 'userinvalid':
2124			case 'passinvalid':
2125				loginMsg('User or Password invalid')
2126				this.userSet('stamp', '')
2127				break
2128
2129			case 'userfail':
2130				loginMsg('Incorrect name or password')
2131				this.userSet('stamp', '')
2132				break
2133
2134			case 'usernot':
2135				loginMsg('No such user')
2136				break
2137
2138			default:
2139				// either it has a mainMsg function or is an ifram that we need to postMessage to
2140				if (typeof mainMsg === 'function') mainMsg(data)
2141				main.iframes.forEach((iframe) => {
2142					iframe.postMessage(data, '*')
2143				})
2144		}
2145
2146		// this lets other code on page also receive socket messages (safe?)
2147		// console.log('fdhfdhfs',typeof mainMsg === 'function')
2148		// if (typeof mainMsg === 'function') mainMsg(data)
2149
2150		// const iframes = document.querySelectorAll('iframe');
2151
2152		// // Loop through them
2153		// iframes.forEach((iframe) => {
2154		// 	// Do something with each iframe, like logging its src attribute
2155		// 	console.log(`Iframe src: ${iframe.src}`, iframe);
2156		// });
2157	}
2158	userSet(name, val) {
2159		localStorage.setItem(`user.${name}`, val)
2160		console.log('userSet', name, val)
2161	}
2162
2163	histAdd(s) {
2164		console.log('hist', s.substring(0, 200))
2165		this.lines = this.lines.slice(Math.max(this.lines.length - 4, 0))
2166		this.lines.push(s)
2167	}
2168
2169	login(name, pass) {
2170		//my.sttTime = performance.now()
2171		let data = { msg: 'login', name: name, pass: pass }
2172		this.send(JSON.stringify(data))
2173		this.histAdd('login: ' + JSON.stringify(data))
2174	}
2175	loginNew(name, pass) {
2176		let data = { msg: 'loginNew', name: name, pass: pass }
2177		this.send(JSON.stringify(data))
2178		this.histAdd('loginNew: ' + JSON.stringify(data))
2179	}
2180	loginPassChg(name, passOld, passNew) {
2181		let data = { msg: 'loginPassChg', name: name, passOld: passOld, passNew }
2182		this.send(JSON.stringify(data))
2183		console.log('loginPassChg: ' + JSON.stringify(data))
2184	}
2185	passHash(pass) {
2186		return '#' + pass + '#'
2187	}
2188}
2189
2190themeSet(themeGet()) // can do before window loaded?
2191window.onload = mainDo
2192
2193main.cache = {
2194	set: function (key, value, ttl) {
2195		const expire = new Date().getTime() + ttl * 1000
2196		const item = { value, expire }
2197		// Serialize the item to JSON and store in localStorage with the key
2198		console.log('cache set', item)
2199		localStorage.setItem('cache.' + key, JSON.stringify(item))
2200	},
2201	get: function (key) {
2202		const itemStr = localStorage.getItem('cache.' + key)
2203		console.log('cache get', itemStr.substring(0, 80))
2204		if (!itemStr) {
2205			return null
2206		}
2207		const item = JSON.parse(itemStr)
2208		return item.value
2209	},
2210	isOK: function (key) {
2211		const itemStr = localStorage.getItem('cache.' + key)
2212		if (!itemStr) {
2213			return false
2214		}
2215		const item = JSON.parse(itemStr)
2216		const currentTime = new Date().getTime()
2217		console.log('cache isOK', currentTime, item.expire, currentTime < item.expire)
2218		if (currentTime < item.expire) {
2219			return true
2220		} else {
2221			// If the item is expired, remove it from localStorage
2222			localStorage.removeItem(key)
2223			return false
2224		}
2225	},
2226}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.