(function (globalScope) { 'use strict'; if (typeof document === 'undefined' || typeof customElements === 'undefined') { return; } const SCRIPT_OPTIONS_DEFAULT = [ { src: 'qhtml.js', checked: true, readOnly: true }, { src: 'q-components.qhtml', checked: true, readOnly: false, kind: 'q-import' }, { src: 'w3-tags.js', checked: true, readOnly: false }, { src: 'bs-tags.js', checked: true, readOnly: false }, { src: 'tools/qhtml-tools.js', checked: true, readOnly: false }, { src: 'q-editor.js', checked: true, readOnly: false }, { src: 'tech-tags.js', checked: false, readOnly: false }, { src: 'lcars-tags.js', checked: false, readOnly: true } ]; const SCRIPT_DEPENDENCIES = Object.freeze({ 'w3-tags.js': Object.freeze({ styles: ['w3.css'], scripts: [] }), 'bs-tags.js': Object.freeze({ styles: ['bs.css'], scripts: ['bs.js'] }) }); const HTML_VOID_TAGS = new Set([ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' ]); const HTML_PRESERVE_TEXT_TAGS = new Set(['pre', 'textarea', 'script', 'style']); const QHTML_VENDOR_PREFIXES = ['w3-', 'bs-', 'uk-', 'mdc-']; const QHTML_KEYWORDS = new Set(['html', 'component', 'q-component', 'slot', 'into', 'q-import', 'bsl']); function formatHTMLFallback(html) { if (!html) return ''; const normalized = String(html).replace(/>\s+<').trim(); const lines = normalized.replace(/>\n<').split('\n'); let indent = 0; const pad = (n) => ' '.repeat(n); const out = []; for (let line of lines) { line = line.trim(); if (!line) continue; const isClosing = /^<\/[A-Za-z]/.test(line); const isOpening = /^<[A-Za-z]/.test(line); const isSelfClosing = /\/>$/.test(line); const tagNameMatch = isOpening ? line.match(/^<([A-Za-z0-9:_-]+)/) : null; const tagName = tagNameMatch ? tagNameMatch[1].toLowerCase() : ''; if (isClosing) indent = Math.max(0, indent - 1); out.push(pad(indent) + line); if (isOpening && !isClosing && !isSelfClosing && tagName && !HTML_VOID_TAGS.has(tagName)) { indent += 1; } } return out.join('\n'); } function stripQhtmlQuotedSections(line) { let result = ''; let quote = ''; let escaped = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (quote) { if (escaped) { escaped = false; continue; } if (ch === '\\') { escaped = true; continue; } if (ch === quote) { quote = ''; } continue; } if (ch === '"' || ch === '\'' || ch === '`') { quote = ch; continue; } result += ch; } return result; } function countLeadingIndentChars(line) { let i = 0; while (i < line.length && (line[i] === ' ' || line[i] === '\t')) i += 1; return i; } function lineOffsets(lines) { const starts = []; let pos = 0; for (let i = 0; i < lines.length; i++) { starts.push(pos); pos += lines[i].length; if (i < lines.length - 1) pos += 1; } return starts; } function lineIndexAtOffset(starts, lines, offset) { let idx = 0; const totalLength = lines.join('\n').length; const clamped = Math.max(0, Math.min(offset, totalLength)); while (idx + 1 < starts.length && starts[idx + 1] <= clamped) idx += 1; return idx; } function formatQhtmlForEditing(source, cursorStart, cursorEnd, protectRadius) { const raw = String(source || '').replace(/\r\n/g, '\n'); if (!raw) { return { text: '', cursorStart: 0, cursorEnd: 0 }; } const lines = raw.split('\n'); const oldStarts = lineOffsets(lines); const protect = new Set(); const safeStart = typeof cursorStart === 'number' ? cursorStart : null; const safeEnd = typeof cursorEnd === 'number' ? cursorEnd : safeStart; const radius = typeof protectRadius === 'number' ? Math.max(0, protectRadius) : 0; if (safeStart !== null && safeEnd !== null && lines.length) { const startLine = lineIndexAtOffset(oldStarts, lines, safeStart); const endLine = lineIndexAtOffset(oldStarts, lines, safeEnd); const lo = Math.max(0, Math.min(startLine, endLine) - radius); const hi = Math.min(lines.length - 1, Math.max(startLine, endLine) + radius); for (let i = lo; i <= hi; i++) protect.add(i); } const newLines = []; const oldLeading = []; const newLeading = []; let depth = 0; for (let idx = 0; idx < lines.length; idx++) { const originalLine = lines[idx]; const trimmed = originalLine.trim(); const oldLead = countLeadingIndentChars(originalLine); oldLeading[idx] = oldLead; if (!trimmed) { newLines.push(''); newLeading[idx] = 0; continue; } let leadingClosers = 0; while (leadingClosers < trimmed.length && trimmed[leadingClosers] === '}') { leadingClosers += 1; } const targetDepth = Math.max(0, depth - leadingClosers); const desiredIndent = ' '.repeat(targetDepth); const content = originalLine.slice(oldLead); const keepAsTyped = protect.has(idx); const formattedLine = keepAsTyped ? originalLine : (desiredIndent + content); newLines.push(formattedLine); newLeading[idx] = keepAsTyped ? oldLead : desiredIndent.length; const analysisLine = stripQhtmlQuotedSections(trimmed).replace(/\/\/.*$/, ''); const opens = (analysisLine.match(/\{/g) || []).length; const closes = (analysisLine.match(/\}/g) || []).length; depth = Math.max(0, depth + opens - closes); } const text = newLines.join('\n'); const newStarts = lineOffsets(newLines); const mapOffset = (offset) => { if (typeof offset !== 'number') return 0; const oldTotal = raw.length; const clamped = Math.max(0, Math.min(offset, oldTotal)); const lineIdx = lineIndexAtOffset(oldStarts, lines, clamped); const oldLineStart = oldStarts[lineIdx]; const newLineStart = newStarts[lineIdx]; const oldLine = lines[lineIdx] || ''; const newLine = newLines[lineIdx] || ''; const oldIndent = oldLeading[lineIdx] || 0; const newIndent = newLeading[lineIdx] || 0; const oldColumn = clamped - oldLineStart; let newColumn; if (oldColumn <= oldIndent) { const deltaFromCodeStart = oldColumn - oldIndent; newColumn = Math.max(0, newIndent + deltaFromCodeStart); } else { newColumn = oldColumn + (newIndent - oldIndent); } newColumn = Math.max(0, Math.min(newColumn, newLine.length)); return Math.max(0, Math.min(newLineStart + newColumn, text.length)); }; return { text, cursorStart: mapOffset(safeStart), cursorEnd: mapOffset(safeEnd) }; } function formatQhtml(source) { return formatQhtmlForEditing(source, null, null, 0).text.trim(); } function formatHTML(html) { const source = String(html || '').trim(); if (!source) return ''; try { const template = document.createElement('template'); template.innerHTML = source; const lines = []; const pad = (n) => ' '.repeat(Math.max(0, n)); const pushLine = (depth, text) => { if (!text) return; lines.push(pad(depth) + text); }; const formatAttributes = (element) => { const attrs = Array.from(element.attributes || []); if (!attrs.length) return ''; return ' ' + attrs.map((attr) => { const safeValue = String(attr.value || '').replace(/"/g, '"'); return attr.name + '="' + safeValue + '"'; }).join(' '); }; const walk = (node, depth, parentTag) => { if (node.nodeType === Node.ELEMENT_NODE) { const tag = node.tagName.toLowerCase(); const attrs = formatAttributes(node); if (HTML_VOID_TAGS.has(tag)) { pushLine(depth, '<' + tag + attrs + '>'); return; } const allChildren = Array.from(node.childNodes || []); const children = allChildren.filter((child) => { if (child.nodeType !== Node.TEXT_NODE) return true; return !!String(child.nodeValue || '').replace(/\s+/g, ' ').trim(); }); if (!children.length) { pushLine(depth, '<' + tag + attrs + '>' + tag + '>'); return; } const inlineTextOnly = children.length === 1 && children[0].nodeType === Node.TEXT_NODE && !HTML_PRESERVE_TEXT_TAGS.has(tag); if (inlineTextOnly) { const text = String(children[0].nodeValue || '').replace(/\s+/g, ' ').trim(); pushLine(depth, '<' + tag + attrs + '>' + text + '' + tag + '>'); return; } pushLine(depth, '<' + tag + attrs + '>'); children.forEach((child) => walk(child, depth + 1, tag)); pushLine(depth, '' + tag + '>'); return; } if (node.nodeType === Node.TEXT_NODE) { const rawText = String(node.nodeValue || ''); if (HTML_PRESERVE_TEXT_TAGS.has(parentTag || '')) { rawText.replace(/\r\n/g, '\n').split('\n').forEach((textLine) => { if (textLine.length) pushLine(depth, textLine); }); } else { const collapsed = rawText.replace(/\s+/g, ' ').trim(); if (collapsed) pushLine(depth, collapsed); } return; } if (node.nodeType === Node.COMMENT_NODE) { const comment = String(node.nodeValue || '').trim(); if (comment) pushLine(depth, ''); } }; Array.from(template.content.childNodes || []).forEach((node) => walk(node, 0, '')); return lines.join('\n').trim(); } catch (err) { return formatHTMLFallback(source); } } function escapeHighlightHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderHighlightTokens(tokens) { let out = ''; for (const token of tokens) { const className = token.type ? 'tok-' + token.type : 'tok-text'; if (token.raw === true) { out += '' + token.value + ''; } else { out += '' + escapeHighlightHtml(token.value) + ''; } } return out; } function tokenizeHtmlForHighlight(input) { const source = String(input || ''); const tokens = []; let index = 0; const len = source.length; const push = (type, value) => { if (value) tokens.push({ type, value }); }; while (index < len) { if (source.startsWith('', index + 4); const stop = end === -1 ? len : end + 3; push('comment', source.slice(index, stop)); index = stop; continue; } if (source[index] === '<') { if (source.startsWith('', index + 2); const stop = end === -1 ? len : end + 1; push('doctype', source.slice(index, stop)); index = stop; continue; } const isClosing = source.startsWith('', index); push('angle', isClosing ? '' : '<'); index += isClosing ? 2 : 1; const nameMatch = /^[A-Za-z][A-Za-z0-9:_-]*/.exec(source.slice(index)); if (!nameMatch) { push('text', source[index] || ''); index += 1; continue; } push('tag', nameMatch[0]); index += nameMatch[0].length; while (index < len) { if (source.startsWith('/>', index)) { push('angle', '/>'); index += 2; break; } if (source[index] === '>') { push('angle', '>'); index += 1; break; } const wsMatch = /^[\s]+/.exec(source.slice(index)); if (wsMatch) { push('text', wsMatch[0]); index += wsMatch[0].length; continue; } const attrMatch = /^[^\s/>=]+/.exec(source.slice(index)); if (!attrMatch) { push('text', source[index]); index += 1; continue; } push('attr', attrMatch[0]); index += attrMatch[0].length; const postNameWs = /^[\s]+/.exec(source.slice(index)); if (postNameWs) { push('text', postNameWs[0]); index += postNameWs[0].length; } if (source[index] === '=') { push('eq', '='); index += 1; const postEqWs = /^[\s]+/.exec(source.slice(index)); if (postEqWs) { push('text', postEqWs[0]); index += postEqWs[0].length; } if (source[index] === '"' || source[index] === '\'') { const quote = source[index]; let end = index + 1; while (end < len && source[end] !== quote) { if (source[end] === '\\' && end + 1 < len) end += 2; else end += 1; } end = end < len ? end + 1 : len; push('string', source.slice(index, end)); index = end; } else { const unquoted = /^[^\s>]+/.exec(source.slice(index)); if (unquoted) { push('string', unquoted[0]); index += unquoted[0].length; } } } } continue; } if (source[index] === '&') { const semi = source.indexOf(';', index + 1); if (semi !== -1) { push('entity', source.slice(index, semi + 1)); index = semi + 1; continue; } } const nextTag = source.indexOf('<', index); const nextEntity = source.indexOf('&', index); let stop = len; if (nextTag !== -1) stop = Math.min(stop, nextTag); if (nextEntity !== -1) stop = Math.min(stop, nextEntity); if (stop === index) stop += 1; push('text', source.slice(index, stop)); index = stop; } return tokens; } function highlightHtmlCode(input) { return renderHighlightTokens(tokenizeHtmlForHighlight(input)); } function tokenizeQhtmlForHighlight(input) { const source = String(input || ''); const tokens = []; let index = 0; let depth = 0; const push = (type, value, raw) => { if (!value) return; tokens.push({ type, value, raw: raw === true }); }; const readQuoted = (quote) => { const start = index; index += 1; while (index < source.length) { const ch = source[index]; index += 1; if (ch === '\\' && index < source.length) { index += 1; continue; } if (ch === quote) break; } return source.slice(start, index); }; while (index < source.length) { const ch = source[index]; if (source.startsWith('//', index)) { const end = source.indexOf('\n', index + 2); const stop = end === -1 ? source.length : end; push('q-comment', source.slice(index, stop)); index = stop; continue; } if (source.startsWith('/*', index)) { const end = source.indexOf('*/', index + 2); const stop = end === -1 ? source.length : end + 2; push('q-comment', source.slice(index, stop)); index = stop; continue; } if (ch === '"' || ch === '\'' || ch === '`') { push('q-string', readQuoted(ch)); continue; } if (/[0-9]/.test(ch)) { const match = /^[0-9][0-9._]*/.exec(source.slice(index)); if (match) { push('q-number', match[0]); index += match[0].length; continue; } } if (ch === '{' || ch === '}') { depth += ch === '{' ? 1 : -1; depth = Math.max(depth, 0); push('q-brace', ch); index += 1; continue; } if (ch === ':') { push('q-colon', ch); index += 1; continue; } if (ch === ';') { push('q-semi', ch); index += 1; continue; } if (ch === ',') { push('q-comma', ch); index += 1; continue; } const wsMatch = /^[\s]+/.exec(source.slice(index)); if (wsMatch) { push('q-value', wsMatch[0]); index += wsMatch[0].length; continue; } const identMatch = /^[A-Za-z_][A-Za-z0-9_.-]*/.exec(source.slice(index)); if (identMatch) { const ident = identMatch[0]; index += ident.length; let lookahead = index; while (lookahead < source.length && /\s/.test(source[lookahead])) lookahead += 1; const next = source[lookahead] || ''; if (ident === 'html' && next === '{') { push('q-kw', ident); if (lookahead > index) { push('q-value', source.slice(index, lookahead)); } push('q-brace', '{'); depth += 1; index = lookahead + 1; const innerStart = index; let innerDepth = 1; while (index < source.length && innerDepth > 0) { if (source[index] === '{') innerDepth += 1; else if (source[index] === '}') innerDepth -= 1; index += 1; } const innerEnd = innerDepth === 0 ? index - 1 : index; const innerHtml = source.slice(innerStart, innerEnd); if (innerHtml) { push('q-embedded', highlightHtmlCode(innerHtml), true); } if (innerDepth === 0) { push('q-brace', '}'); depth = Math.max(0, depth - 1); } continue; } if (QHTML_KEYWORDS.has(ident)) { push('q-kw', ident); } else if (QHTML_VENDOR_PREFIXES.some((prefix) => ident.startsWith(prefix))) { push('q-class', ident); } else if (next === ':') { push('q-prop', ident); } else if (next === '{' || next === ',') { push('q-selector', ident); } else { push(depth > 0 ? 'q-value' : 'q-selector', ident); } continue; } push('q-value', ch); index += 1; } return tokens; } function highlightQhtmlCode(input) { return renderHighlightTokens(tokenizeQhtmlForHighlight(input)); } function getQhtmlToolsApi() { if (globalScope.qhtmlTools && typeof globalScope.qhtmlTools.toHTML === 'function') return globalScope.qhtmlTools; if (globalScope['qhtml-tools'] && typeof globalScope['qhtml-tools'].toHTML === 'function') return globalScope['qhtml-tools']; if (globalScope.qhtml && typeof globalScope.qhtml.toHTML === 'function') return globalScope.qhtml; return null; } function removeNewBodyQHtmlNodes(beforeSet) { const nodes = document.querySelectorAll('body > q-html'); nodes.forEach((node) => { if (!beforeSet.has(node)) node.remove(); }); } async function renderHtmlFromQhtml(source, scratch) { if (!source) return ''; const qhtmlSource = String(source).trim().replace(/^"|"$/g, ''); const toolsApi = getQhtmlToolsApi(); if (toolsApi && typeof toolsApi.toHTML === 'function') { const before = new Set(Array.from(document.querySelectorAll('body > q-html'))); try { const html = await Promise.resolve(toolsApi.toHTML(qhtmlSource)); return html || ''; } finally { removeNewBodyQHtmlNodes(before); } } const scratchEl = scratch || document.createElement('q-html'); if (typeof scratchEl.preprocess !== 'function' || typeof scratchEl.parseQHtml !== 'function') { return source; } const pre = await Promise.resolve(scratchEl.preprocess(qhtmlSource)); const html = scratchEl.parseQHtml(pre); const regex = /"{1}([^\"]*)"{1}/mg; return html.replace(regex, (match, p1) => '"' + decodeURIComponent(p1) + '"'); } function escapeAttr(value) { return String(value || '') .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } function basenamePath(value) { const source = String(value || ''); const slash = source.lastIndexOf('/'); return slash >= 0 ? source.slice(slash + 1) : source; } function resolveSiblingPath(basePath, siblingName) { const source = String(basePath || ''); const slash = source.lastIndexOf('/'); if (slash < 0) return siblingName; return source.slice(0, slash + 1) + siblingName; } class QEditor extends HTMLElement { constructor() { super(); this._activeTab = 'qhtml'; this._qhtmlSource = ''; this._htmlRaw = ''; this._htmlOutput = ''; this._renderVersion = 0; this._scratchQhtml = document.createElement('q-html'); this._previewHost = null; this._qhtmlFormatTimer = null; this._isAutoFormattingQhtml = false; this._scripts = SCRIPT_OPTIONS_DEFAULT.map((item) => Object.assign({}, item)); this._mounted = false; this._startupRenderSynced = false; this._awaitingQhtmlDefinition = false; this._deferredOutputQueued = false; this._startupRenderListener = null; } connectedCallback() { if (this._mounted) return; this._mounted = true; const initialFromAttr = this.getAttribute('initial-qhtml'); const initialFromBody = this.textContent || ''; const initialSource = initialFromAttr != null ? String(initialFromAttr) : initialFromBody; this.textContent = ''; this._renderShell(); this._cacheDomNodes(); this._renderScriptsList(); this._bindEvents(); this._setActiveTab('qhtml'); this.setQhtmlSource(initialSource); this._ensureStartupRenderPass(); } disconnectedCallback() { this._mounted = false; if (this._qhtmlFormatTimer) { clearTimeout(this._qhtmlFormatTimer); this._qhtmlFormatTimer = null; } if (this._startupRenderListener) { document.removeEventListener('QHTMLContentLoaded', this._startupRenderListener); this._startupRenderListener = null; } this._startupRenderSynced = false; this._awaitingQhtmlDefinition = false; this._deferredOutputQueued = false; } setQhtmlSource(text) { this._qhtmlSource = formatQhtml(text); if (this._qhtmlInput) this._qhtmlInput.value = this._qhtmlSource; this._updateQhtmlHighlight(); this._updateOutputs(); this._scheduleOutputRetry(); } getQhtmlSource() { return this._qhtmlSource; } _scheduleOutputRetry() { if (!this.isConnected) return; const schedule = (cb) => { if (typeof globalScope.requestAnimationFrame === 'function') { globalScope.requestAnimationFrame(cb); return; } setTimeout(cb, 0); }; if (!customElements.get('q-html')) { if (this._awaitingQhtmlDefinition || typeof customElements.whenDefined !== 'function') return; this._awaitingQhtmlDefinition = true; customElements.whenDefined('q-html') .then(() => { this._awaitingQhtmlDefinition = false; if (!this.isConnected) return; this._scratchQhtml = document.createElement('q-html'); this._updateOutputs(); }) .catch(() => { this._awaitingQhtmlDefinition = false; }); return; } if (this._deferredOutputQueued) return; this._deferredOutputQueued = true; schedule(() => { this._deferredOutputQueued = false; if (!this.isConnected) return; this._scratchQhtml = document.createElement('q-html'); this._updateOutputs(); }); } _ensureStartupRenderPass() { if (this._startupRenderSynced) return; this._startupRenderSynced = true; this._scheduleOutputRetry(); if (typeof document !== 'undefined' && !this._startupRenderListener) { this._startupRenderListener = () => { this._startupRenderListener = null; this._scheduleOutputRetry(); }; document.addEventListener('QHTMLContentLoaded', this._startupRenderListener, { once: true }); } } _renderShell() { this.innerHTML = '' + '
Available Scripts
' + '' + '