diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..08f6bb9 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2025-02-25 - Avoid leaving random test files when generating patches +**Learning:** Even when working carefully on isolated performance optimizations using a temporary Gradle setup (`tempTest/`) or ad-hoc scripts (`patch_hint.sh`), it's incredibly important to clean up these files before requesting a code review or submitting a PR. Leaving them pollutes the git working tree, turning an otherwise excellent PR into a blockable state. +**Action:** Before running `request_code_review` or `submit`, explicitly run a `git status` and actively `rm -rf` all scripts, test files, and temporary directories that are not intended to be part of the final commit. diff --git a/halogen-core/src/commonMain/kotlin/halogen/SchemaParser.kt b/halogen-core/src/commonMain/kotlin/halogen/SchemaParser.kt index 4c15627..212069e 100644 --- a/halogen-core/src/commonMain/kotlin/halogen/SchemaParser.kt +++ b/halogen-core/src/commonMain/kotlin/halogen/SchemaParser.kt @@ -15,8 +15,6 @@ public object SchemaParser { isLenient = true } - private val HEX_COLOR_REGEX: Regex = Regex("^#[0-9A-Fa-f]{6}$") - /** * Parse a JSON string (potentially wrapped in markdown code fences) into * a validated [HalogenThemeSpec]. @@ -67,7 +65,7 @@ public object SchemaParser { ) for ((name, value) in colorFields) { - if (!HEX_COLOR_REGEX.matches(value)) { + if (!isValidHexColor(value)) { return Result.failure( IllegalArgumentException( "Invalid hex color for $name: \"$value\". Expected format: #RRGGBB", @@ -85,4 +83,13 @@ public object SchemaParser { return Result.success(clamped) } + + private fun isValidHexColor(value: String): Boolean { + if (value.length != 7 || value[0] != '#') return false + for (i in 1 until 7) { + val c = value[i] + if (c !in '0'..'9' && c !in 'a'..'f' && c !in 'A'..'F') return false + } + return true + } } diff --git a/halogen-engine/src/commonMain/kotlin/halogen/engine/HintExtractor.kt b/halogen-engine/src/commonMain/kotlin/halogen/engine/HintExtractor.kt index da90f01..d3783f9 100644 --- a/halogen-engine/src/commonMain/kotlin/halogen/engine/HintExtractor.kt +++ b/halogen-engine/src/commonMain/kotlin/halogen/engine/HintExtractor.kt @@ -10,42 +10,78 @@ package halogen.engine */ internal object HintExtractor { - private val PREFIX_PATTERN = Regex("""^(?:/r/|/category/|/topic/|/|#)""") - private val CAMEL_SPLIT = Regex("""(?<=[a-z])(?=[A-Z])""") - private val ID_PATTERN = Regex("""^[0-9a-f]{8,}$""", RegexOption.IGNORE_CASE) - private val NUMERIC_ONLY = Regex("""^\d+$""") - private val WHITESPACE_PATTERN = Regex("""\s+""") - fun extract(key: String): String? { if (key.isBlank()) return null + val trimmed = key.trim() + var start = 0 + val len = trimmed.length // Strip common prefixes - var cleaned = PREFIX_PATTERN.replace(key.trim(), "") + if (trimmed.startsWith("/r/")) start = 3 + else if (trimmed.startsWith("/category/")) start = 10 + else if (trimmed.startsWith("/topic/")) start = 7 + else if (trimmed.startsWith("/") || trimmed.startsWith("#")) start = 1 - // Remove leading/trailing slashes - cleaned = cleaned.trim('/') + var end = len + while (start < end && trimmed[start] == '/') start++ + while (start < end && trimmed[end - 1] == '/') end-- // Take the last meaningful segment if it looks like a path - if ('/' in cleaned) { - cleaned = cleaned.substringAfterLast('/') + val lastSlash = trimmed.lastIndexOf('/', end - 1) + if (lastSlash >= start) { + start = lastSlash + 1 } - // Split camelCase - cleaned = CAMEL_SPLIT.replace(cleaned, " ") + if (start >= end) return null + + val sb = StringBuilder() + var hasValidChar = false + var isAllHex = true + var isAllDigit = true + var charCount = 0 + var spacePending = false + var prevWasLower = false + + for (i in start until end) { + val c = trimmed[i] + if (c == '_' || c == '-' || c.isWhitespace()) { + if (hasValidChar) { + spacePending = true + } + prevWasLower = false + } else { + val isUpper = c.isUpperCase() + val isLower = c.isLowerCase() - // Split snake_case and kebab-case - cleaned = cleaned.replace('_', ' ').replace('-', ' ') + // Split camelCase + if (prevWasLower && isUpper) { + spacePending = true + } - // Normalize whitespace - cleaned = cleaned.trim().replace(WHITESPACE_PATTERN, " ") + if (spacePending && hasValidChar) { + sb.append(' ') + spacePending = false + } - if (cleaned.isBlank()) return null + sb.append(c.lowercaseChar()) + hasValidChar = true + prevWasLower = isLower + charCount++ + + // Validation checks + if (c !in '0'..'9') { + isAllDigit = false + if (c !in 'a'..'f' && c !in 'A'..'F') { + isAllHex = false + } + } + } + } - // Reject things that look like IDs - val noSpaces = cleaned.replace(" ", "") - if (ID_PATTERN.matches(noSpaces)) return null - if (NUMERIC_ONLY.matches(noSpaces)) return null + if (!hasValidChar) return null + if (isAllDigit) return null + if (isAllHex && charCount >= 8) return null - return cleaned.lowercase() + return sb.toString() } }