package com.sptmobile.render import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.withStyle // [impl->REQ-RICH-RENDER] /** * Tiny XML/HTML syntax highlighter — a token colorizer, NOT a validating parser * (malformed markup degrades to plain monospace, never throws). Clean-room; the * spt `` frames and agent-emitted XML are the target. Token colors ride * in via [MarkdownStyles] so light/dark themes both read well. * * Tokens: ``, tag punctuation + names (``, * `>`), attribute names, and `="quoted values"`; free text between tags is left * default-colored. Everything is monospace. */ fun highlightXml(code: String, styles: MarkdownStyles): AnnotatedString = buildAnnotatedString { appendXml(this, code, styles) } internal fun appendXml(b: AnnotatedString.Builder, code: String, styles: MarkdownStyles) { b.withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) { var i = 0 while (i < code.length) { when { code.startsWith("", i) val stop = if (end < 0) code.length else end + 3 withStyle(SpanStyle(color = styles.xmlComment)) { append(code.substring(i, stop)) } i = stop } code[i] == '<' -> { val end = code.indexOf('>', i) val stop = if (end < 0) code.length else end + 1 appendTag(this, code.substring(i, stop), styles) i = stop } else -> { val next = code.indexOf('<', i) val stop = if (next < 0) code.length else next append(code.substring(i, stop)) // free text: default color i = stop } } } } } /** One `<…>` tag: punctuation + name in [MarkdownStyles.xmlTag], attribute names * in [MarkdownStyles.xmlAttr], `="values"` in [MarkdownStyles.xmlValue]. */ private fun appendTag(b: AnnotatedString.Builder, tag: String, styles: MarkdownStyles) { // Strip the outer < … > (or < … />), color them, tokenize the inside. A tag // with no closing '>' (unterminated, e.g. a truncated frame) must round-trip // its exact text — never synthesize a '>'. val hasClose = tag.endsWith(">") val inner = tag.removePrefix("<").let { if (hasClose) it.removeSuffix(">") else it } val selfClose = hasClose && inner.endsWith("/") val body = if (selfClose) inner.dropLast(1) else inner val closing = body.startsWith("/") val afterSlash = if (closing) body.drop(1) else body val nameEnd = afterSlash.indexOfFirst { it.isWhitespace() } val name = if (nameEnd < 0) afterSlash else afterSlash.substring(0, nameEnd) val attrs = if (nameEnd < 0) "" else afterSlash.substring(nameEnd) b.withStyle(SpanStyle(color = styles.xmlTag)) { append(if (closing) "" else ">") } } } private fun appendAttrs(b: AnnotatedString.Builder, attrs: String, styles: MarkdownStyles) { var i = 0 while (i < attrs.length) { val c = attrs[i] when { c.isWhitespace() -> { b.append(c); i++ } c == '"' || c == '\'' -> { val end = attrs.indexOf(c, i + 1) val stop = if (end < 0) attrs.length else end + 1 b.withStyle(SpanStyle(color = styles.xmlValue)) { append(attrs.substring(i, stop)) } i = stop } c == '=' -> { b.withStyle(SpanStyle(color = styles.xmlTag)) { append(c) } i++ } else -> { // attribute name run: up to whitespace or '=' var j = i while (j < attrs.length && !attrs[j].isWhitespace() && attrs[j] != '=') j++ b.withStyle(SpanStyle(color = styles.xmlAttr)) { append(attrs.substring(i, j)) } i = j } } } }