/*
* Copyright 2026 The Ray Optics Simulation authors and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
import escapeHtml from 'escape-html'
import {
findPrimitiveCurveReferenceSpans
} from '../../core/primitive/diagnosticReference.js'
/** Inner length (between `{` and `}`) above which we collapse the segment in the UI. */
export const JSON_INNER_COLLAPSE_THRESHOLD = 48
/**
* HTML ignores newline characters in text; convert them to line breaks for status display.
* @param {string} html
* @returns {string}
*/
function newlinesToBr(html) {
return html.replace(/\r\n|\r|\n/g, '<br>')
}
/**
* Index of the matching `}` for `{` at openIdx, or -1. Ignores `{` / `}` inside JSON strings.
* @param {string} s
* @param {number} openIdx
* @returns {number}
*/
export function findMatchingBraceEnd(s, openIdx) {
if (openIdx < 0 || openIdx >= s.length || s[openIdx] !== '{') return -1
let depth = 1
let inString = false
let i = openIdx + 1
while (i < s.length) {
const c = s[i]
if (inString) {
if (c === '\\') {
i++
if (i >= s.length) return -1
if (s[i] === 'u') {
i++
for (let k = 0; k < 4; k++) {
if (i >= s.length) return -1
if (!/[0-9a-fA-F]/.test(s[i])) return -1
i++
}
continue
}
i++
continue
}
if (c === '"') {
inString = false
}
i++
continue
}
if (c === '"') {
inString = true
i++
continue
}
if (c === '{') depth++
else if (c === '}') {
depth--
if (depth === 0) return i
}
i++
}
return -1
}
/**
* Non-overlapping `{ ... }` spans that parse as JSON.
* @param {string} line
* @returns {Array<{start: number, end: number, json: string}>}
*/
export function findValidJsonObjectSpans(line) {
const spans = []
let i = 0
while (i < line.length) {
if (line[i] === '{') {
const end = findMatchingBraceEnd(line, i)
if (end === -1) {
i++
continue
}
const json = line.slice(i, end + 1)
try {
JSON.parse(json)
spans.push({ start: i, end: end, json })
} catch {
/* not valid JSON */
}
i = end + 1
} else {
i++
}
}
return spans
}
const TOGGLE_ICON_SVG =
'<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="status-alert__json-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 0 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 0 1 0-3 1.5 1.5 0 0 1 0 3z"/></svg>'
/**
* HTML for one status line with long JSON segments collapsed to `{ [icon] }`
* and primitive-curve references rendered as interactive controls.
* @param {string} line
* @param {object} options
* @param {Record<string, boolean>} options.expanded
* @param {string} options.keyPrefix e.g. 'w' or 'e'
* @param {number} options.lineIdx
* @param {object} options.labels
* @param {string} options.labels.expand
* @param {function(number): string} [options.labels.primitiveCurve]
* @param {number} [options.threshold]
* @returns {string}
*/
export function formatStatusLineHtml(line, options) {
const {
expanded,
keyPrefix,
lineIdx,
labels,
threshold = JSON_INNER_COLLAPSE_THRESHOLD
} = options
const jsonSpans = findValidJsonObjectSpans(line).map(span => ({
...span,
kind: 'json'
}))
const primitiveCurveSpans = findPrimitiveCurveReferenceSpans(line)
.filter(span => !jsonSpans.some(
jsonSpan => span.start >= jsonSpan.start && span.end <= jsonSpan.end
))
.map(span => ({ ...span, kind: 'primitiveCurve' }))
const spans = [...jsonSpans, ...primitiveCurveSpans]
.sort((a, b) => a.start - b.start)
if (spans.length === 0) {
return newlinesToBr(escapeHtml(line))
}
let out = ''
let last = 0
let jsonSpanIdx = 0
for (const span of spans) {
out += escapeHtml(line.slice(last, span.start))
if (span.kind === 'primitiveCurve') {
const tip = escapeHtml(
labels.primitiveCurve?.(span.curveId) ??
`Primitive curve ${span.curveId}`
)
out += `<button type="button" class="status-alert__primitive-curve-ref" data-primitive-curve-id="${span.curveId}" title="${tip}" aria-label="${tip}">${span.curveId}</button>`
} else {
const inner = line.slice(span.start + 1, span.end)
const key = `${keyPrefix}-${lineIdx}-${jsonSpanIdx}`
const collapsed = inner.length > threshold
if (!collapsed) {
out += escapeHtml(span.json)
} else if (expanded[key]) {
out += `<span class="status-alert__json-expanded">${escapeHtml(span.json)}</span>`
} else {
const tip = escapeHtml(labels.expand)
out += `{<button type="button" class="status-alert__json-toggle" data-json-key="${escapeHtml(key)}" title="${tip}" aria-label="${tip}" aria-expanded="false">${TOGGLE_ICON_SVG}</button>}`
}
jsonSpanIdx++
}
last = span.end + 1
}
out += escapeHtml(line.slice(last))
return newlinesToBr(out)
}