(function () { 'use strict'; var source = document.getElementById('editor'); if (!source) return; var config = JSON.parse(document.getElementById('sandbox-config').value); var dataInput = document.getElementById('sandbox-data'); var output = document.getElementById('set_result'); var button = document.getElementById('get_result'); var status = document.getElementById('loading'); var errorBox = document.getElementById('sandbox-error'); var frame = document.getElementById('sandbox-preview'); var select = document.getElementById('sandbox-example'); var revision = 0; var hasResult = false; var editor = window.CodeMirror ? window.CodeMirror.fromTextArea(source, { lineNumbers: true, indentWithTabs: true, tabSize: 2, // Existing tokenizer; no optional HTML overlay dependencies needed. mode: 'django:inner', viewportMargin: 10, extraKeys: { Esc: function () { dataInput.focus(); } } }) : null; if (editor) { editor.getInputField().setAttribute('aria-label', config.code); editor.getInputField().setAttribute('aria-describedby', 'sandbox-hint'); source.labels[0].addEventListener('click', function () { editor.focus(); }); } function getCode() { return editor ? editor.getValue() : source.value; } function setCode(value) { if (editor) editor.setValue(value); else source.value = value; } function announce(message) { status.textContent = message; } function clearResult(message) { revision += 1; hasResult = false; output.value = ''; frame.removeAttribute('srcdoc'); errorBox.hidden = true; document.getElementById('sandbox-copy-output').disabled = true; document.getElementById('sandbox-empty').hidden = false; announce(message); } function changed() { clearResult(config.stale); } if (editor) editor.on('change', changed); else source.addEventListener('input', changed); dataInput.addEventListener('input', changed); // Template contents remain inert. Rebuild only static elements, never input // nodes, attributes, URLs, CSS or handlers. The iframe adds a second boundary. function safeHTML(value) { var template = document.createElement('template'); template.innerHTML = value; var container = document.createElement('div'); var allowed = new Set('article section div span p h1 h2 h3 h4 h5 h6 strong em b i s del u small ul ol li table caption thead tbody tfoot tr th td pre code blockquote hr br'.split(' ')); var dropped = new Set('script style iframe object embed link meta base svg math template noscript audio video source img picture form input button textarea select option'.split(' ')); var count = 0; function append(node, parent, depth) { if (++count > 10000 || depth > 60) return; if (node.nodeType === Node.TEXT_NODE) { parent.appendChild(document.createTextNode(node.textContent)); return; } if (node.nodeType !== Node.ELEMENT_NODE) return; var tag = node.localName.toLowerCase(); if (dropped.has(tag)) return; var target = parent; if (allowed.has(tag)) { target = document.createElement(tag); parent.appendChild(target); } Array.from(node.childNodes).forEach(function (child) { append(child, target, depth + 1); }); } Array.from(template.content.childNodes).forEach(function (node) { append(node, container, 0); }); return '' + '' + '' + container.innerHTML + ''; } function loadExample() { var example = config.examples[Number(select.value)]; setCode(example.code); dataInput.value = JSON.stringify(example.data, null, 2); document.getElementById('sandbox-description').textContent = example.description; document.getElementById('sandbox-location').textContent = example.location; document.getElementById('sandbox-try').textContent = example.try; document.getElementById('sandbox-example-docs').href = example.reference; clearResult(config.reset_done); } select.addEventListener('change', loadExample); document.getElementById('sandbox-reset').addEventListener('click', loadExample); function errorMessage(error, kind) { var message = String(error.message || error); var category = kind === 'json' ? config.json_error : config.liquid_error; if (/undefined variable|UndefinedVariable/.test(message + error.name)) category = config.missing_error; if (/undefined filter/.test(message)) category = config.filter_error; if (error.name === 'SandboxRestrictionError' || /external templates/.test(message)) category = config.restricted_error; if (/limit exceeded|too large|too long/i.test(message)) category = config.limit_error; var position = error.token && typeof error.token.getPosition === 'function' ? error.token.getPosition() : null; return category + (position ? ' ' + config.line + ' ' + position[0] + '.' : '') + '\n' + message; } function restricted() { var error = new Error('This sandbox does not load external templates.'); error.name = 'SandboxRestrictionError'; throw error; } async function run() { var current = ++revision; var kind = 'json'; button.disabled = true; output.setAttribute('aria-busy', 'true'); errorBox.hidden = true; announce(config.loading); try { if (getCode().length > 100000 || dataInput.value.length > 250000) throw new Error('Input too large.'); var context = JSON.parse(dataInput.value); if (!context || Array.isArray(context) || typeof context !== 'object') throw new Error(config.json_object); kind = 'liquid'; if (!window.liquidjs) throw new Error(config.engine_missing); var engine = new window.liquidjs.Liquid({ strictVariables: true, strictFilters: true, ownPropertyOnly: true, renderLimit: 1000, parseLimit: 1000, memoryLimit: 10000000, fs: { resolve: restricted, readFile: restricted, readFileSync: restricted, exists: restricted, existsSync: restricted } }); ['include', 'render', 'layout'].forEach(function (name) { engine.registerTag(name, { parse: restricted }); }); var rendered = await engine.parseAndRender(getCode(), context); if (current !== revision) return; if (rendered.length > 250000) throw new Error('Output too large.'); output.value = rendered; frame.srcdoc = safeHTML(rendered); hasResult = true; document.getElementById('sandbox-copy-output').disabled = false; document.getElementById('sandbox-empty').hidden = true; announce(config.success); } catch (error) { if (current !== revision) return; output.value = ''; frame.removeAttribute('srcdoc'); hasResult = false; document.getElementById('sandbox-copy-output').disabled = true; document.getElementById('sandbox-empty').hidden = true; errorBox.textContent = errorMessage(error, kind); errorBox.hidden = false; announce(errorBox.textContent.split('\n')[0]); } finally { button.disabled = false; output.removeAttribute('aria-busy'); } } button.addEventListener('click', run); async function copy(value, field) { try { await navigator.clipboard.writeText(value); announce(config.copied); } catch (_) { if (field === source && editor) { editor.focus(); editor.execCommand('selectAll'); } else if (field) { field.focus(); field.select(); } announce(config.copy_failed); } } document.getElementById('sandbox-copy-code').addEventListener('click', function () { copy(getCode(), source); }); document.getElementById('sandbox-copy-output').addEventListener('click', function () { if (hasResult) copy(output.value, output); }); Array.from(document.querySelectorAll('input[name="sandbox-view"]')).forEach(function (input) { input.addEventListener('change', function () { document.getElementById('sandbox-text-panel').hidden = input.value !== 'text'; document.getElementById('sandbox-html-panel').hidden = input.value !== 'html'; }); }); loadExample(); run(); }());