import { TAnyFixme } from "@haibun/core/build/lib/fixme.js"; function escapeHtml(unsafe: string): string { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function isSimpleAggregate(value: TAnyFixme): boolean { if (typeof value !== 'object' || value === null) { return false; } if (Array.isArray(value)) { if (value.length === 0) return true; return value.every(item => typeof item !== 'object' || item === null); } else { const keys = Object.keys(value); if (keys.length === 0) return true; return keys.every(key => typeof value[key] !== 'object' || value[key] === null); } } function getPrimitiveString(value: TAnyFixme, key?: string | number): string { if (key === 'html') { return ''; } if (typeof value === 'string') { return JSON.stringify(value); } if (value === null) return 'null'; return String(value); } function getSimpleAggregateString(value: TAnyFixme): string { if (Array.isArray(value)) { return `[${value.map(item => getPrimitiveString(item)).join(', ')}]`; } else if (typeof value === 'object' && value !== null) { const parts = Object.entries(value).map(([k, v]) => { let primStr = getPrimitiveString(v, k); if (k === 'html') { primStr = escapeHtml(primStr); } return `"${k}": ${primStr}`; }); return `{ ${parts.join(', ')} }`; } return String(value); } function createInlineDisplayForSimpleAggregate(aggregateValue: TAnyFixme): HTMLElement { const containerSpan = document.createElement('span'); containerSpan.className = 'simple-aggregate-inline'; if (Array.isArray(aggregateValue)) { containerSpan.appendChild(document.createTextNode('[')); aggregateValue.forEach((item, index) => { containerSpan.appendChild(createPrimitiveNode(item)); if (index < aggregateValue.length - 1) { containerSpan.appendChild(document.createTextNode(', ')); } }); containerSpan.appendChild(document.createTextNode(']')); } else if (typeof aggregateValue === 'object' && aggregateValue !== null) { containerSpan.appendChild(document.createTextNode('{ ')); const keys = Object.keys(aggregateValue); keys.forEach((key, index) => { const keySpan = document.createElement('span'); keySpan.className = 'key inline-key'; keySpan.textContent = `"${key}": `; containerSpan.appendChild(keySpan); containerSpan.appendChild(createPrimitiveNode(aggregateValue[key], key)); if (index < keys.length - 1) { containerSpan.appendChild(document.createTextNode(', ')); } }); containerSpan.appendChild(document.createTextNode(' }')); } return containerSpan; } export function disclosureJson(jsonObj: TAnyFixme): HTMLElement | undefined { if (typeof jsonObj !== 'object' || jsonObj === null) { const primitiveContainer = document.createElement('div'); primitiveContainer.className = 'json-root-primitive'; primitiveContainer.appendChild(createPrimitiveNode(jsonObj)); primitiveContainer.dataset.rawJson = JSON.stringify(jsonObj); return primitiveContainer; } if (isSimpleAggregate(jsonObj)) { const simpleRootContainer = document.createElement('div'); simpleRootContainer.className = 'json-root-simple-aggregate'; simpleRootContainer.appendChild(createInlineDisplayForSimpleAggregate(jsonObj)); simpleRootContainer.dataset.rawJson = JSON.stringify(jsonObj); return simpleRootContainer; } else { const rootContainer = document.createElement('div'); rootContainer.className = 'json-root-complex'; rootContainer.dataset.rawJson = JSON.stringify(jsonObj); const isRootArray = Array.isArray(jsonObj); if (isRootArray) { (jsonObj as TAnyFixme[]).forEach((item, index) => { appendNode(index, item, true, rootContainer, 0, undefined); }); } else { Object.keys(jsonObj).forEach(key => { appendNode(key, jsonObj[key], false, rootContainer, 0, undefined); }); } return rootContainer; } } function appendNode(key: string | number, value: TAnyFixme, isArrayIndex: boolean, parentElement: HTMLElement, indentLevel: number = 0, parentKey?: string | number): void { const isPrimitiveVal = typeof value !== 'object' || value === null; if (isPrimitiveVal) { const lineDiv = document.createElement('div'); lineDiv.className = 'json-line primitive-kv'; let marginLeftPx = 0; if (indentLevel === 1) { marginLeftPx = 20; } else if (indentLevel > 1) { marginLeftPx = 20 + (indentLevel - 1) * 15; } lineDiv.style.marginLeft = `${marginLeftPx}px`; const keySpan = document.createElement('span'); keySpan.className = isArrayIndex ? 'key index' : 'key'; keySpan.textContent = isArrayIndex ? `[${key}]: ` : `${key}: `; lineDiv.appendChild(keySpan); let useTextArea = false; if (isPrimitiveVal) { if (key === 'in' && typeof value === 'string') { useTextArea = true; } else if (key === 'named') { useTextArea = true; } else if (parentKey === 'named' || parentKey === 'namedWithVars') { useTextArea = true; } } if (useTextArea) { const textArea = document.createElement('textarea'); textArea.classList.add('expanded-value-textarea'); textArea.value = String(value); let pathForId = 'root'; if (parentElement.parentElement && parentElement.parentElement.tagName === 'DETAILS') { pathForId = getPathToNode(parentElement.parentElement as HTMLElement); } const keyString = String(key).replace(/[^a-zA-Z0-9_-]/g, '_'); textArea.id = `textarea-${pathForId}-${keyString}`; textArea.rows = 1; const setInitialWidth = (element: HTMLTextAreaElement) => { let helper = document.getElementById('autogrow-helper') as HTMLDivElement | null; if (!helper) { helper = document.createElement('div'); helper.id = 'autogrow-helper'; helper.style.position = 'absolute'; helper.style.left = '-9999px'; helper.style.top = '0'; helper.style.visibility = 'hidden'; helper.style.pointerEvents = 'none'; document.body.appendChild(helper); } const computedStyle = window.getComputedStyle(element); helper.style.font = computedStyle.font; helper.style.letterSpacing = computedStyle.letterSpacing; helper.style.paddingTop = computedStyle.paddingTop; helper.style.paddingRight = computedStyle.paddingRight; helper.style.paddingBottom = computedStyle.paddingBottom; helper.style.paddingLeft = computedStyle.paddingLeft; helper.style.borderTopWidth = computedStyle.borderTopWidth; helper.style.borderRightWidth = computedStyle.borderRightWidth; helper.style.borderBottomWidth = computedStyle.borderBottomWidth; helper.style.borderLeftWidth = computedStyle.borderLeftWidth; helper.style.borderStyle = computedStyle.borderStyle; helper.style.boxSizing = 'border-box'; helper.style.lineHeight = computedStyle.lineHeight; helper.style.whiteSpace = 'nowrap'; helper.style.width = 'auto'; helper.textContent = element.value || ' '; let contentWidth = helper.scrollWidth; contentWidth += 2; element.style.width = contentWidth + 'px'; }; const handleInputResize = (element: HTMLTextAreaElement) => { element.style.height = 'auto'; element.style.height = element.scrollHeight + 'px'; }; textArea.addEventListener('input', () => handleInputResize(textArea)); lineDiv.appendChild(textArea); lineDiv.classList.add('line-with-textarea'); parentElement.appendChild(lineDiv); requestAnimationFrame(() => { setInitialWidth(textArea); }); } else { lineDiv.appendChild(createPrimitiveNode(value, key)); parentElement.appendChild(lineDiv); } } else { const forceComplex = key === 'named' || key === 'namedWithVars'; if (!forceComplex && isSimpleAggregate(value)) { const lineDiv = document.createElement('div'); lineDiv.className = 'json-line simple-aggregate-kv'; let marginLeftPx = 0; if (indentLevel === 1) { marginLeftPx = 20; } else if (indentLevel > 1) { marginLeftPx = 20 + (indentLevel - 1) * 15; } lineDiv.style.marginLeft = `${marginLeftPx}px`; const keySpan = document.createElement('span'); keySpan.className = isArrayIndex ? 'key index' : 'key'; keySpan.textContent = isArrayIndex ? `[${key}]: ` : `${key}: `; lineDiv.appendChild(keySpan); lineDiv.appendChild(createInlineDisplayForSimpleAggregate(value)); parentElement.appendChild(lineDiv); } else { const details = document.createElement('details'); details.className = (isArrayIndex ? 'json-array-item' : 'json-object-item') + ' expandable'; let marginLeftPx = 0; if (indentLevel === 1) { marginLeftPx = 20; } else if (indentLevel > 1) { marginLeftPx = 20 + (indentLevel - 1) * 15; } details.style.marginLeft = `${marginLeftPx}px`; details.open = false; const summary = document.createElement('summary'); const summaryContentSpan = document.createElement('span'); const keyDisplayPart = document.createElement('span'); keyDisplayPart.className = isArrayIndex ? 'key index' : 'key'; keyDisplayPart.textContent = isArrayIndex ? `[${String(key)}]: ` : `${String(key)}: `; summaryContentSpan.appendChild(keyDisplayPart); const childrenSummarySpan = document.createElement('span'); childrenSummarySpan.className = 'children-summary-inline'; const summaryChildrenParts = []; if (Array.isArray(value)) { (value as TAnyFixme[]).forEach((childValue, childIndex) => { const childKeyString = String(childIndex); const isChildValuePrimitive = typeof childValue !== 'object' || childValue === null; if (isChildValuePrimitive) { summaryChildrenParts.push(`{${childKeyString}: ${getPrimitiveString(childValue, childIndex)}}`); } else if (isSimpleAggregate(childValue)) { summaryChildrenParts.push(`{${childKeyString}: ${getSimpleAggregateString(childValue)}}`); } else { summaryChildrenParts.push(`+${childKeyString}`); } }); } else { Object.entries(value).forEach(([childKey, childValue]) => { const isChildValuePrimitive = typeof childValue !== 'object' || childValue === null; if (isChildValuePrimitive) { let primDisplayStr = getPrimitiveString(childValue, childKey); if (childKey === 'html') { primDisplayStr = escapeHtml(primDisplayStr); } summaryChildrenParts.push(`{${childKey}: ${primDisplayStr}}`); } else if (isSimpleAggregate(childValue)) { summaryChildrenParts.push(`{${childKey}: ${getSimpleAggregateString(childValue)}}`); } else { summaryChildrenParts.push(`+${childKey}`); } }); } childrenSummarySpan.innerHTML = summaryChildrenParts.join(', '); summaryContentSpan.appendChild(childrenSummarySpan); summary.appendChild(summaryContentSpan); details.appendChild(summary); const detailsContentDiv = document.createElement('div'); detailsContentDiv.className = 'details-content'; if (Array.isArray(value)) { (value as TAnyFixme[]).forEach((item, childIndex) => { appendNode(childIndex, item, true, detailsContentDiv, indentLevel + 1, key); }); } else { Object.entries(value).forEach(([childKey, childValue]) => { appendNode(childKey, childValue, false, detailsContentDiv, indentLevel + 1, key); }); } details.appendChild(detailsContentDiv); details.addEventListener('toggle', () => { if (details.open) { childrenSummarySpan.classList.add('hidden'); } else { childrenSummarySpan.classList.remove('hidden'); } }); if (!details.open) { childrenSummarySpan.classList.remove('hidden'); } else { childrenSummarySpan.classList.add('hidden'); } parentElement.appendChild(details); } } } function getPathToNode(element: HTMLElement): string { const parts: string[] = []; while (element && element.parentElement && !element.classList.contains('json-root-complex')) { const keySpan = element.querySelector(':scope > summary > span > .key'); let key = keySpan?.textContent?.replace(/: $/, '') || 'item'; if (element.classList.contains('json-array-item')) { key = element.querySelector(':scope > summary > span > .key.index')?.textContent?.replace(/[[\]:]/g, '') || 'index'; } parts.unshift(key); element = element.parentElement.closest('details, .json-root-complex'); } return parts.join('-'); } function createPrimitiveNode(value: TAnyFixme, key?: string | number): HTMLSpanElement { const span = document.createElement('span'); span.classList.add('value'); let displayValue: string; let typeClass: string = typeof value; if (key === 'html') { displayValue = ''; typeClass = 'html'; } else if (value === null) { typeClass = 'null'; displayValue = ''; } else if (typeClass === 'string') { displayValue = JSON.stringify(value); } else if (typeClass === 'undefined') { displayValue = 'undefined'; } else { displayValue = String(value); } span.classList.add(`value-${typeClass}`); span.textContent = displayValue; return span; }