package cc.unitmesh.devins.parser /** * 代码围栏解析器 - 核心解析逻辑 * 从 IDEA 插件版本移植,去除 IntelliJ Platform 依赖 */ class CodeFence( val languageId: String, val text: String, var isComplete: Boolean, val extension: String? = null ) { companion object { private var lastTxtBlock: CodeFence? = null val devinStartRegex = Regex("") val devinEndRegex = Regex("") val thinkingStartRegex = Regex("") val thinkingEndRegex = Regex("") val walkthroughStartRegex = Regex("") val walkthroughEndRegex = Regex("") fun parse(content: String): CodeFence { val languageRegex = Regex("\\s*```([\\w#+ ]*)") val lines = content.lines() // Check for tag first val devinStartMatch = devinStartRegex.find(content) if (devinStartMatch != null) { val endMatch = devinEndRegex.find(content) val isComplete = endMatch != null val devinContent = if (isComplete) { content.substring(devinStartMatch.range.last + 1, endMatch!!.range.first).trim() } else { content.substring(devinStartMatch.range.last + 1).trim() } return CodeFence("devin", devinContent, isComplete, "devin") } // Check for tag val thinkingStartMatch = thinkingStartRegex.find(content) if (thinkingStartMatch != null) { val endMatch = thinkingEndRegex.find(content) val isComplete = endMatch != null val thinkingContent = if (isComplete) { content.substring(thinkingStartMatch.range.last + 1, endMatch!!.range.first).trim() } else { content.substring(thinkingStartMatch.range.last + 1).trim() } return CodeFence("thinking", thinkingContent, isComplete, "thinking") } // Check for HTML comment val walkthroughStartMatch = walkthroughStartRegex.find(content) if (walkthroughStartMatch != null) { val endMatch = walkthroughEndRegex.find(content) val isComplete = endMatch != null val walkthroughContent = if (isComplete) { content.substring(walkthroughStartMatch.range.last + 1, endMatch!!.range.first).trim() } else { content.substring(walkthroughStartMatch.range.last + 1).trim() } return CodeFence("walkthrough", walkthroughContent, isComplete, "walkthrough") } var codeStarted = false var codeClosed = false var languageId: String? = null var codeIndentation = "" val codeBuilder = StringBuilder() for (line in lines) { if (!codeStarted) { val trimmedLine = line.trimStart() val matchResult: MatchResult? = languageRegex.find(trimmedLine) if (matchResult != null) { // Store the indentation to match it when looking for the closing fence codeIndentation = line.substring(0, line.length - trimmedLine.length) val substring = matchResult.groups[1]?.value?.trim() languageId = substring codeStarted = true } } else { val trimmedLine = line.trimStart() if (trimmedLine == "```") { codeClosed = true break } else { codeBuilder.append(line).append("\n") } } } val trimmedCode = codeBuilder.trim().toString() val extension = lookupFileExt(languageId ?: "txt") return if (trimmedCode.isEmpty()) { CodeFence(languageId ?: "", "", codeClosed, extension) } else { CodeFence(languageId ?: "", trimmedCode, codeClosed, extension) } } /** * 解析所有代码块(包括文本块和代码块) */ fun parseAll(content: String): List { val codeFences = mutableListOf() var currentIndex = 0 var processedContent = content if (content.contains("```devin\n")) { processedContent = preProcessDevinBlock(content) } // Parse , and tags val tagMatches = mutableListOf>() devinStartRegex.findAll(processedContent).forEach { tagMatches.add("devin" to it) } thinkingStartRegex.findAll(processedContent).forEach { tagMatches.add("thinking" to it) } walkthroughStartRegex.findAll(processedContent).forEach { tagMatches.add("walkthrough" to it) } // Sort by position tagMatches.sortBy { it.second.range.first } for ((tagType, startMatch) in tagMatches) { if (startMatch.range.first >= currentIndex) { if (startMatch.range.first > currentIndex) { val beforeText = processedContent.substring(currentIndex, startMatch.range.first) if (beforeText.trim().isNotEmpty()) { parseMarkdownContent(beforeText, codeFences) } } // Find the corresponding end tag val endRegex = when (tagType) { "devin" -> devinEndRegex "thinking" -> thinkingEndRegex "walkthrough" -> walkthroughEndRegex else -> devinEndRegex } val endMatch = endRegex.find(processedContent, startMatch.range.last + 1) val isComplete = endMatch != null val tagContent = if (isComplete) { processedContent.substring(startMatch.range.last + 1, endMatch!!.range.first).trim() } else { processedContent.substring(startMatch.range.last + 1).trim() } codeFences.add(CodeFence(tagType, tagContent, isComplete, tagType)) currentIndex = if (isComplete) { endMatch!!.range.last + 1 } else { processedContent.length } } } if (currentIndex < processedContent.length) { val remainingContent = processedContent.substring(currentIndex) if (remainingContent.trim().isNotEmpty()) { parseMarkdownContent(remainingContent, codeFences) } } return codeFences.filter { if (it.languageId == "devin" || it.languageId == "thinking" || it.languageId == "walkthrough") { return@filter true } return@filter it.text.isNotEmpty() } } val devinRegexBlock = Regex("(?<=^|\\n)```devin\\n([\\s\\S]*?)\\n```\\n") val normalCodeBlock = Regex("\\s*```([\\w#+ ]*)\\n") fun preProcessDevinBlock(content: String): String { var currentContent = content val devinMatches = devinRegexBlock.findAll(content).toList() for (match in devinMatches) { var devinContent = match.groups[1]?.value ?: "" if (normalCodeBlock.find(devinContent) != null) { if (!devinContent.trim().endsWith("```")) { devinContent += "\n```" } } val replacement = "\n\n$devinContent\n" currentContent = currentContent.replace(match.value, replacement) } return currentContent } private fun parseMarkdownContent(content: String, codeFences: MutableList) { val languageRegex = Regex("\\s*```([\\w#+ ]*)") val lines = content.lines() var codeStarted = false var languageId: String? = null val codeBuilder = StringBuilder() val textBuilder = StringBuilder() var codeIndentation = "" for (i in lines.indices) { val line = lines[i] if (!codeStarted) { // Check for code block start with any indentation val trimmedLine = line.trimStart() val matchResult = languageRegex.find(trimmedLine) if (matchResult != null) { if (textBuilder.isNotEmpty()) { val textBlock = CodeFence( "markdown", textBuilder.trim().toString(), true, "md" ) lastTxtBlock = textBlock codeFences.add(textBlock) textBuilder.clear() } // Store the indentation to match it when looking for the closing fence codeIndentation = line.substring(0, line.length - trimmedLine.length) languageId = matchResult.groups[1]?.value?.trim() codeStarted = true } else { textBuilder.append(line).append("\n") } } else { // Check if this line contains the closing fence val trimmedLine = line.trimStart() if (trimmedLine == "```") { val codeContent = codeBuilder.trim().toString() val codeFence = CodeFence( languageId ?: "markdown", codeContent, true, lookupFileExt(languageId ?: "md") ) codeFences.add(codeFence) codeBuilder.clear() codeStarted = false languageId = null codeIndentation = "" } else { codeBuilder.append(line).append("\n") } } } if (textBuilder.isNotEmpty()) { val textBlock = CodeFence("markdown", textBuilder.trim().toString(), true, "md") codeFences.add(textBlock) } if (codeStarted && codeBuilder.isNotEmpty()) { val code = codeBuilder.trim().toString() val codeFence = CodeFence( languageId ?: "markdown", code, false, lookupFileExt(languageId ?: "md") ) codeFences.add(codeFence) } } /** * 根据语言 ID 查找文件扩展名 */ fun lookupFileExt(languageId: String): String { return when (languageId.lowercase()) { "c#", "csharp" -> "cs" "c++", "cpp" -> "cpp" "c" -> "c" "java" -> "java" "javascript", "js" -> "js" "kotlin", "kt" -> "kt" "python", "py" -> "py" "ruby", "rb" -> "rb" "swift" -> "swift" "typescript", "ts" -> "ts" "markdown", "md" -> "md" "sql" -> "sql" "plantuml", "puml" -> "puml" "shell", "bash", "sh" -> "sh" "objective-c" -> "m" "objective-c++" -> "mm" "go" -> "go" "html" -> "html" "css" -> "css" "dart" -> "dart" "scala" -> "scala" "rust", "rs" -> "rs" "http request", "http" -> "http" "shell script" -> "sh" "devin" -> "devin" "json" -> "json" "yaml", "yml" -> "yaml" "xml" -> "xml" "toml" -> "toml" "dockerfile" -> "dockerfile" else -> languageId } } /** * 根据文件扩展名获取显示名称 */ fun displayNameByExt(extension: String): String { return when (extension.lowercase()) { "cs" -> "C#" "cpp" -> "C++" "c" -> "C" "java" -> "Java" "js" -> "JavaScript" "kt" -> "Kotlin" "py" -> "Python" "rb" -> "Ruby" "swift" -> "Swift" "ts" -> "TypeScript" "md" -> "Markdown" "sql" -> "SQL" "puml" -> "PlantUML" "sh" -> "Shell Script" "m" -> "Objective-C" "mm" -> "Objective-C++" "go" -> "Go" "html" -> "HTML" "css" -> "CSS" "dart" -> "Dart" "scala" -> "Scala" "rs" -> "Rust" "http" -> "HTTP Request" "json" -> "JSON" "yaml" -> "YAML" "xml" -> "XML" "toml" -> "TOML" else -> extension.uppercase() } } } }