NodeList.prototype.forEach = NodeList.prototype.forEach || Array.prototype.forEach; HTMLCollection.prototype.forEach = HTMLCollection.prototype.forEach || Array.prototype.forEach; DOMTokenList.prototype.forEach = DOMTokenList.prototype.forEach || Array.prototype.forEach; HTMLSelectElement.prototype.getSelected = function(){ var s = []; for (var i = 0; i < this.length; i++) { if (this[i].selected) s.push(this[i].value); } return s; }; String.prototype.ucFirst = function(){ return this.charAt(0).toUpperCase() + this.slice(1); }; function $(s){ return document.querySelector(s); } function $$(s){ return document.querySelectorAll(s); } var evalOrg = {}; (function() { "use strict"; var self = this, refreshTimer, refreshCount = 0, perfAggregates = undefined; this.editor = undefined; this.livePush = undefined; this.initialize = function() { window.addEventListener('error', this.postError) this.applyPrefs(); this.enableFocus(); $$('a[href^="http"]').forEach(function (el) { el.setAttribute('target', '_blank'); el.setAttribute('rel', 'noopener'); }); if ($('h2.exception')) document.body.classList.add('error'); document.body.classList.forEach(function (c) { if ('function' == typeof this['handle' + c.ucFirst()]) window.addEventListener('load', this['handle' + c.ucFirst()].bind(this)); }.bind(this)); $$('.alert').forEach(function (el) { el.addEventListener('touchstart', function () { el.remove(); }); }); }; var loadScript = function(url, f) { var s = document.createElement('script'); s.setAttribute('src', url); s.onload = function () { if ('function' == typeof f) f(); document.head.removeChild(this) }.bind(s); document.head.appendChild(s); }; this.postError = function(e) { // https://www.ravikiranj.net/posts/2014/code/how-fix-cryptic-script-error-javascript/ if (e.message === 'Script error.') return; // Googlebot throws this when using NodeList.forEach if (e.message === 'Uncaught TypeError: undefined is not a function') return; // Yandex through HeadlessChrome, but not reproducible if (e.message === 'Uncaught ') return; var xhr = new XMLHttpRequest(); xhr.open('post', '/javascript-error/' + encodeURIComponent(e.message)); xhr.send(); }; this.applyDarkmode = function(enable) { if (enable) { document.documentElement.classList.add('darkMode'); if (this.editor) this.editor.setTheme('ace/theme/chaos'); } else { document.documentElement.classList.remove('darkMode'); if (this.editor) this.editor.setTheme('ace/theme/chrome'); } }; this.applyPrefs = function() { var defaul = false; if (localStorage.getItem("darkMode") === "enable") defaul = true; else if (localStorage.getItem("darkMode") === "disable") defaul = false; else if (window.matchMedia('(prefers-color-scheme: dark)').matches) defaul = true; this.applyDarkmode(defaul); if (defaul) $('#darkMode').setAttribute('checked', 'checked'); $('#darkMode').addEventListener('change', function (e) { localStorage.setItem('darkMode', e.target.checked ? 'enable' : 'disable'); this.applyDarkmode(e.target.checked) }.bind(this)); if (localStorage.getItem("livePreview") !== "disable") $('#livePreview').setAttribute('checked', 'checked'); $('#livePreview').addEventListener('change', function (e) { localStorage.setItem('livePreview', e.target.checked ? 'enable' : 'disable'); }); } this.richEditor = function() { if (this.editor) return false; var code = $('code'); var textarea = $('textarea[name=code]'); textarea.value = code.textContent; // If ace somehow doesn't load; make sure js doesn't crash if ('object' != typeof ace) document.body.classList.add('mobile'); // Disable ace for mobile-devices; see https://github.com/ajaxorg/ace/issues/37 if (document.body.classList.contains('mobile')) return; // Use a shim to keep ff happy ace.config.set('workerPath', '/s/'); ace.require('ace/ext/language_tools'); this.editor = ace.edit(code); if (document.documentElement.classList.contains('darkMode')) this.editor.setTheme('ace/theme/chaos'); else this.editor.setTheme('ace/theme/chrome'); this.editor.setShowPrintMargin(false); this.editor.setOption('maxLines', Infinity); this.editor.session.setMode('ace/mode/php'); this.editor.session.setUseWrapMode(true); this.editor.setOptions({ enableBasicAutocompletion: true, enableLiveAutocompletion: false }); $$('textarea.ace_text-input').forEach(function(el){ el.setAttribute('aria-label', textarea.getAttribute('aria-label')); }); if ($('input[type=submit]')) $('input[type=submit]').setAttribute('disabled', 'disabled'); $('#newForm').addEventListener('submit', function(){ textarea.value = this.editor.getValue(); }.bind(this)); this.editor.on('change', function(){ $('#newForm').classList.add('changed'); if (!$('#tabs.abusive')) $('input[type=submit]').removeAttribute('disabled'); if ($('#live_preview')) { if ('number' == typeof this.livePush) clearInterval(this.livePush); return this.livePush = setTimeout(this.livePreviewPush.bind(this), 500); } if ($('#livePreview').checked) this.livePreviewCreate(); }.bind(this)); }; this.livePreviewCreate = function() { history.pushState({}, 'preview', '/#live'); outputClearTabs(); $('#tabs').classList.add('busy'); $('#tabs').appendChild(object2Dom({ li: { 'class': 'active', a: {href: '/#live', _text: 'Live-preview'} } })); $('#tab').appendChild(object2Dom({ dl:{ dt: {_text: "Output for php 8.1.12"}, dd: { div: { id: 'live_preview', 'data-cfg': $('base').getAttribute('href')+'live/x86.cfg', } } } })); _launchVm('init=/sbin/preview'); // every 250ms, check if _malloc is defined (vm running) so we can push var check = setInterval(function(){ if ('undefined' == typeof _malloc) return; if ('number' == typeof this.livePush) clearInterval(this.livePush); clearInterval(check); this.livePush = setTimeout(this.livePreviewPush.bind(this), 250); }.bind(this), 250); }; this.livePreviewPush = function(){ if (this.editor) var content = this.editor.getValue(); else var content = $('textarea[name=code]').value; var buf = new TextEncoder("utf-8").encode(content); var buf_len = buf.length; var buf_addr = _malloc(buf_len); HEAPU8.set(buf, buf_addr); $('#tabs').classList.add('busy'); window.fs_import_file('preview', buf_addr, buf_len); }; var outputClearTabs = function() { if (!$('#tabs')) { $$('#newForm ~ div.column').forEach(function(div){ div.parentNode.removeChild(div); }); $('#newForm').parentNode.insertBefore(object2Dom({div: {id: 'tab'}}), $('#newForm').nextSibling); $('#newForm').parentNode.insertBefore(object2Dom({ul: {id: 'tabs'}}), $('#tab')); } else { while ($('#tabs').firstChild) $('#tabs').removeChild($('#tabs').firstChild); while ($('#tab').firstChild) $('#tab').removeChild($('#tab').firstChild); } }; this.handleScript = function() { if ('undefined' != typeof refreshTimer) return; if ($('#tabs.busy')) refreshTimer = setInterval(this.refresh, 1000); this.localTime(function(el, d, t){ el.innerHTML = ' @ '+ d +' '+ t; }, 'input + time'); }; // Triggered on /new errorpage, eg. title too long this.handleNew = function() { this.richEditor(); if ($('#tabs.abusive')) { if ($('#version')) $('#version').remove(); // 2/3d of all js errors from from here, where this.editor is undefined if ('undefined' != typeof this.editor) this.editor.setReadOnly(true); } document.body.addEventListener('keydown', function(e){ // cancel ctrl/cmd+s > prevent browser from saving page if (83 == e.which && (e.ctrlKey || e.metaKey)) { e.preventDefault(); return false; } if ($('input[type=submit][disabled]') || 13 != e.keyCode) return; var event = new Event('submit', { 'view': window, 'bubbles': true, 'cancelable': true }); // None of the handlers called preventDefault. if (e.ctrlKey && $('#newForm').dispatchEvent(event)) $('#newForm').submit(); }.bind(this)); // Attempt to reload a preview window.onpopstate = this.previewStateLoad.bind(this); }; this.handleRfc = function() { return this.handleOutput(); }; this.handleOutput = function() { $$('dt').forEach(function(el){ el.addEventListener('click', function(){ window.location.hash = '#'+ el.id; }); }); outputAddExpander(); outputAddDiff(); outputAsHtml(); }; var outputAddExpander = function() { if (document.body.classList.contains('touch') || $('#expand')) return; var hasOverflow = false; $$('dd').forEach(function(dd){ hasOverflow = hasOverflow || dd.scrollHeight>dd.clientHeight; hasOverflow = hasOverflow || dd.scrollWidth>dd.clientWidth; }); if (!hasOverflow) return; $('div#tab').insertBefore(object2Dom({ a: { id: 'expand', title: 'expand output', i: { 'class': 'icon-resize-full expand' } } }), $('div#tab').firstChild); $('#expand').addEventListener('click', outputExpand); }; var outputExpand = function() { $('dl').classList.toggle('expand'); $('a#expand i').classList.toggle('icon-resize-full'); $('a#expand i').classList.toggle('icon-resize-small'); }; var outputAddDiff = function() { if ($('#diff') || $$('#tab dd').length < 2) return; $('div#tab').insertBefore(object2Dom({ a: { id: 'diff', title: 'diff output', i: {'class': 'icon-tasks'} } }), $('div#tab').firstChild); $('#diff').addEventListener('click', outputDiff); }; var diffDone = false; var outputDiff = function() { var ref = $$('div#tab dt:target + dd'); ref = ref[0] || $$('div#tab dd:first-of-type')[0]; $$('div#tab dd').forEach(function (dd){ if (!dd.hasAttribute('original')) dd.setAttribute('original', dd.innerHTML); if (diffDone) dd.innerHTML = dd.getAttribute('original'); else if (dd != ref) { var fragment = document.createDocumentFragment(), node, swap; var diff = JsDiff.diffWordsWithSpace( ref.hasChildNodes() ? ref.childNodes[0].textContent : '', dd.hasChildNodes() ? dd.childNodes[0].textContent : '' ); for (var i=0; i < diff.length; i++) { if (diff[i].added && diff[i + 1] && diff[i + 1].removed) { swap = diff[i]; diff[i] = diff[i + 1]; diff[i + 1] = swap; } if (diff[i].removed) { node = document.createElement('del'); node.appendChild(document.createTextNode(diff[i].value)); } else if (diff[i].added) { node = document.createElement('ins'); node.appendChild(document.createTextNode(diff[i].value)); } else node = document.createTextNode(diff[i].value); fragment.appendChild(node); } // No output means childnodes is empty if (!dd.hasChildNodes()) dd.appendChild(fragment); else { dd.childNodes[0].textContent = ''; dd.insertBefore(fragment, dd.childNodes[0]); } } }); $('a#diff i').classList.toggle('active'); diffDone = !diffDone; }; var outputAsHtml = function() { if ($('#asHtml')) return; $('div#tab').insertBefore(object2Dom({ a: { id: 'asHtml', title: 'interpret as HTML', i: {'class': 'icon-eye-open'} } }), $('div#tab').firstChild); $('#asHtml').addEventListener('click', outputHtml); }; var outputHtml = function() { $('a#asHtml i').classList.toggle('active'); var toggleEnable = $('a#asHtml i').classList.contains('active'); $$('div#tab dd').forEach(function (dd){ if (!dd.hasAttribute('original')) dd.setAttribute('original', dd.innerHTML); if (toggleEnable) dd.innerHTML = dd.innerText; else dd.innerHTML = dd.getAttribute('original'); }); }; var focusVersion = ''; var focusIsBranch = false; this.enableFocus = function() { this.fillVersionSelector(); if ($('#newForm')) $('#newForm').addEventListener('submit', this.preview.bind(this)); // b/c if (0 === window.location.hash.indexOf('#focus=')) window.location.hash = 'v'+ window.location.hash.substr('#focus='.length); if (-1 === window.location.hash.indexOf('#v')) return; // verify the hash is a valid version var version = window.location.hash.substr(2), option = $('#version option[value="' + version + '"]'); if (version.match(/^[.0-9a-z_-]+$/) && option) { focusVersion = version; focusIsBranch = 'branches' === option.parentElement.label; option.setAttribute('selected', 'selected'); outputHighlightVersion(version) // disable live preview when we are focused on another single version $('#livePreview').removeAttribute('checked'); } }; var outputHighlightVersion = function(version) { $$('#tab dl dt').forEach(function (dt) { var dtTitle = dt.textContent; // for rfcs if (-1 !== dtTitle.indexOf(version)) dt.setAttribute('id', 'v'+version) else if (version.split('.').length === 3) { var l = version.match(/(\d\.\d.)(\d+)/), vPrefix = l[1], vReleaseStr = l[2]; var vRelease = Number(vReleaseStr) var matches = Array.from(dtTitle.matchAll(/([.\d]+)(?: - ([\d.]+))?/g)); for (var i = 0; i < matches.length; i++) { var rangeMin = matches[i][1], rangeMax = matches[i][2]; // console.log(vPrefix, vRelease, rangeMin.substr(vPrefix.length), rangeMax.substr(vPrefix.length)); if ( rangeMin.startsWith(vPrefix) && Number(rangeMin.substr(vPrefix.length)) <= vRelease && rangeMax.startsWith(vPrefix) && Number(rangeMax.substr(vPrefix.length)) >= vRelease ) dt.setAttribute('id', 'v'+version) } } }); }; this.fillVersionSelector = function() { if (!$('#version')) return; var select = $('#version'); var versions = JSON.parse(select.dataset['values']); var majors = Object.keys(versions); var addOpt = function (g, v){ var o = document.createElement('option'); o.setAttribute('value', v); o.appendChild(document.createTextNode(v)); g.appendChild(o); }; var getGroup = function(l){ var group = $('#version optgroup[label="'+l+'"]'); if (!group) { group = document.createElement('optgroup'); group.label = l; select.appendChild(group); } return group } // Enforce the order of these select.appendChild(object2Dom({'option': {value: '', _text: 'all supported versions'}})); select.appendChild(object2Dom({'option': {value: 'eol', _text: '+ include eol (slow)'}})); var current = select.appendChild(object2Dom({'optgroup': {label: 'current'}})); var branches = select.appendChild(object2Dom({'optgroup': {label: 'branches'}})); majors.forEach(function(key, idx) { if (key[1] != '.') var group = branches else var group = getGroup((key.substr(-1) === '.') ? key.substr(0, key.length-1) : key); if (typeof versions[key] === 'number') for (var i = versions[key]; i >= 0; i--) addOpt(group, key + i); else if (typeof versions[key] === 'object') versions[key].forEach(function(v){addOpt(group, key + v);}); else addOpt(group, key + versions[key]); // assume there are 3 supported major versions and filter out betas if (idx <=2 && key.length === 4) addOpt(current, group.firstChild.textContent); }); // Move select to end of form $('#newForm').appendChild(select); select.addEventListener('change', function(){ $('input[type=submit]').removeAttribute('disabled'); focusVersion = $('#version').value; focusIsBranch = $('#version').selectedOptions[0].parentNode.label == 'branches'; outputHighlightVersion(focusVersion) document.location.hash = '#v'+ focusVersion; }); $('#newForm').addEventListener('submit', this.preview.bind(this)); }; this.preview = function(e) { if (!$('#version') || $('#version').value == '' || $('#version').value == 'eol') return; var xhr = new XMLHttpRequest(); xhr.onload = _refreshFocus; xhr.open('post', '/new'); xhr.setRequestHeader('Accept', 'application/json'); outputClearTabs(); // The response takes time; provide feedback about being in progress $('#tabs').appendChild(object2Dom({ li: { 'class': 'active', a: {href: '/#preview', _text: 'Preview'} } })); $('#tab').appendChild(document.createElement('dl')); $('#tabs').classList.add('busy'); // is this required? if (this.editor) $('textarea[name=code]').value = this.editor.getValue(); xhr.send(new FormData($('#newForm'))); e.preventDefault(); return false; }; this.previewStateLoad = function(e) { // Not every hash change means there is a valid state to pop if (!e.state) return; if (e.state.code) this.editor.setValue(e.state.code, 1); if (e.state.version) $('#version').value = e.state.version; // get results by resubmit / replaceState storing output ? }; this.refresh = function() { if (!$('#tabs.busy') || refreshCount > 42) { window.clearInterval(refreshTimer); $('#tabs').classList.remove('busy'); return; } refreshCount++; var xhr = new XMLHttpRequest(); xhr.onload = _refresh; xhr.open('get', window.location.pathname); if (document.body.classList.contains('output')) { xhr.open('get', window.location.pathname+'.json'); xhr.setRequestHeader('Accept', 'application/json'); xhr.onload = _refreshOutput; xhr.timeout = 500; } xhr.send(); }; var _refresh = function() { var tab = $('#tab'); var t = this.responseText.match(/