gnodet-bot commented on code in PR #27253: URL: https://github.com/apache/camel/pull/27253#discussion_r4163365016
########## dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteRefactorings.java: ########## @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.tui; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The refactorings of the source editor (Ctrl+R) for Java and XML routes (CAMEL-25256), as {@link SourceRefactorings} + * has them for YAML: the endpoint URI of a line, the string literal (Java) or attribute value (XML) at the cursor, and + * the element block of an XML step, with what replaces them. + */ +final class RouteRefactorings { + + /** + * A quoted value on a line: where its text starts and ends (inside the quotes), and the text as the route means it + * (escapes and entities undone). + */ + record Value(int start, int end, String text) { + } + + /** The DSL methods (Java) and elements (XML) whose argument or uri attribute is an endpoint URI. */ + private static final String ENDPOINT_CALLS = "from|to|toD|wireTap|enrich|pollEnrich|poll"; + private static final Pattern JAVA_URI = Pattern.compile( + "\\b(?:" + ENDPOINT_CALLS + ")\\s*\\(\\s*\"((?:[^\"\\\\]|\\\\.)*)\""); + private static final Pattern XML_URI = Pattern.compile( + "<(?:[\\w-]+:)?(?:" + ENDPOINT_CALLS + ")\\b[^>]*?\\suri\\s*=\\s*(\"([^\"]*)\"|'([^']*)')"); + private static final Pattern XML_ATTRIBUTE = Pattern.compile("[\\w:.-]+\\s*=\\s*(\"([^\"]*)\"|'([^']*)')"); + + /** Elements that are no step to extract: the route, its input, and the parts of the steps that hold them. */ + private static final Set<String> NOT_EXTRACTABLE = Set.of( + "routes", "route", "camel", "beans", "from", "when", "otherwise", "doCatch", "doFinally", "onFallback", + "expression", "simple", "constant", "header", "exchangeProperty", "variable", "jsonpath", "xpath", + "groovy", "language", "method", "tokenize", "jq", "ref", "datasonnet", "spel", "mvel", "ognl", "js", + "python", "java", "wasm", "xquery", "xtokenize", "hl7terser", "jactl", "csimple", "joor"); + + private RouteRefactorings() { + } + + // ---- the endpoint URI of a line ---- + + /** The endpoint URI on the line: the string of from, to, toD... in Java, the uri attribute of those in XML. */ + static Value uri(String dsl, String line) { + if (line == null) { + return null; + } + Matcher m = ("xml".equals(dsl) ? XML_URI : JAVA_URI).matcher(line); + if (!m.find()) { + return null; + } + if ("xml".equals(dsl)) { + int group = m.group(2) != null ? 2 : 3; + return new Value(m.start(group), m.end(group), xmlDecode(m.group(group))); + } + return new Value(m.start(1), m.end(1), javaUnescape(m.group(1))); + } + + // ---- the value at the cursor ---- + + /** The string literal (Java) or attribute value (XML) the cursor is on; null when it is on none. */ + static Value valueAt(String dsl, String line, int col) { + if (line == null || col < 0) { + return null; + } + if ("xml".equals(dsl)) { + Matcher m = XML_ATTRIBUTE.matcher(line); + while (m.find()) { + int group = m.group(2) != null ? 2 : 3; + // on the attribute name or in its value, the quotes included + if (col >= m.start() && col <= m.end()) { + return new Value(m.start(group), m.end(group), xmlDecode(m.group(group))); + } + } + return null; + } + int open = -1; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (open < 0 && c == '/' && i + 1 < line.length() && line.charAt(i + 1) == '/') { + return null; + } + if (open >= 0 && c == '\\') { + i++; + } else if (c == '"') { + if (open < 0) { + open = i; + } else { + if (col >= open && col <= i + 1) { + return new Value(open + 1, i, javaUnescape(line.substring(open + 1, i))); + } + open = -1; + } + } + } + return null; + } + + /** Whether a value can be extracted to a property: something, and no placeholder already. */ + static boolean isExtractable(Value v) { + return v != null && !v.text().isBlank() && !(v.text().startsWith("{{") && v.text().endsWith("}}")); + } + + /** The line with the value replaced, written as the DSL quotes it (escaped in Java, entities in XML). */ + static String replace(String dsl, String line, Value v, String text) { + String written = "xml".equals(dsl) ? xmlEncode(text) : javaEscape(text); + return line.substring(0, v.start()) + written + line.substring(v.end()); + } + + /** + * The application.properties a property of the route file goes to: src/main/resources of a Maven or Gradle layout, + * else the file's own folder, as camel run reads it. + */ + static Path propertiesFile(Path routeFile) { + Path dir = routeFile.toAbsolutePath().getParent(); + for (Path p = dir; p != null; p = p.getParent()) { + if (p.getFileName() != null && "src".equals(p.getFileName().toString())) { + return p.resolve("main/resources/application.properties"); + } + } + return dir.resolve("application.properties"); + } + + // ---- an XML step block ---- + + /** The rows of an XML element block, its start tag on the first. */ + record Block(int start, int end, String element, int indent) { + } + + private static final Pattern START_TAG = Pattern.compile("^(\\s*)<(?:[\\w-]+:)?([\\w-]+)"); + + /** The step block whose start tag is on the row; null when the row holds none, or no step to extract. */ + static Block xmlStep(List<String> lines, int row) { + if (row < 0 || row >= lines.size()) { + return null; + } + Matcher m = START_TAG.matcher(lines.get(row)); + if (!m.find() || NOT_EXTRACTABLE.contains(m.group(2))) { + return null; + } + // must be inside a route + XmlCompletionContext c = XmlCompletionContext.at(lines, row, m.start(2) - 1); + if (c == null || !c.path().contains("route")) { + return null; + } + int depth = 0; + for (int r = row; r < lines.size(); r++) { + depth += tagBalance(lines.get(r)); + if (depth <= 0) { + return new Block(row, r, m.group(2), m.group(1).length()); + } + } + return null; + } + + /** + * The opening minus the closing tags of a line: self-closing tags, comments and processing instructions count 0. + */ + static int tagBalance(String line) { + int balance = 0; + String s = line.replaceAll("<!--.*?-->", "").replaceAll("<\\?.*?\\?>", ""); + Matcher m = Pattern.compile("<(/?)[\\w:-]+[^>]*?(/?)>").matcher(s); Review Comment: ⚠️ **Medium — Performance:** `tagBalance` compiles a new `Pattern` on every invocation (line 181: `Pattern.compile("<(/?)[\\w:-]+[^>]*?(/?)>")` and the two `replaceAll` calls at lines 180-181 also compile patterns internally). This method is called per-line in the `xmlStep` loop, so for a large XML route file it creates 3 pattern objects per line scanned. Consider hoisting these to `private static final Pattern` fields, like the other patterns in this class. ```suggestion private static final Pattern COMMENT = Pattern.compile("<!--.*?-->"); private static final Pattern PI = Pattern.compile("<\\?.*?\\?>"); private static final Pattern TAG = Pattern.compile("<(/?)[\\w:-]+[^>]*?(/?)>"); static int tagBalance(String line) { int balance = 0; String s = COMMENT.matcher(line).replaceAll(""); s = PI.matcher(s).replaceAll(""); Matcher m = TAG.matcher(s); ``` ########## dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteRefactorings.java: ########## @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.tui; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The refactorings of the source editor (Ctrl+R) for Java and XML routes (CAMEL-25256), as {@link SourceRefactorings} + * has them for YAML: the endpoint URI of a line, the string literal (Java) or attribute value (XML) at the cursor, and + * the element block of an XML step, with what replaces them. + */ +final class RouteRefactorings { + + /** + * A quoted value on a line: where its text starts and ends (inside the quotes), and the text as the route means it + * (escapes and entities undone). + */ + record Value(int start, int end, String text) { + } + + /** The DSL methods (Java) and elements (XML) whose argument or uri attribute is an endpoint URI. */ + private static final String ENDPOINT_CALLS = "from|to|toD|wireTap|enrich|pollEnrich|poll"; + private static final Pattern JAVA_URI = Pattern.compile( + "\\b(?:" + ENDPOINT_CALLS + ")\\s*\\(\\s*\"((?:[^\"\\\\]|\\\\.)*)\""); + private static final Pattern XML_URI = Pattern.compile( + "<(?:[\\w-]+:)?(?:" + ENDPOINT_CALLS + ")\\b[^>]*?\\suri\\s*=\\s*(\"([^\"]*)\"|'([^']*)')"); + private static final Pattern XML_ATTRIBUTE = Pattern.compile("[\\w:.-]+\\s*=\\s*(\"([^\"]*)\"|'([^']*)')"); + + /** Elements that are no step to extract: the route, its input, and the parts of the steps that hold them. */ + private static final Set<String> NOT_EXTRACTABLE = Set.of( + "routes", "route", "camel", "beans", "from", "when", "otherwise", "doCatch", "doFinally", "onFallback", + "expression", "simple", "constant", "header", "exchangeProperty", "variable", "jsonpath", "xpath", + "groovy", "language", "method", "tokenize", "jq", "ref", "datasonnet", "spel", "mvel", "ognl", "js", + "python", "java", "wasm", "xquery", "xtokenize", "hl7terser", "jactl", "csimple", "joor"); + + private RouteRefactorings() { + } + + // ---- the endpoint URI of a line ---- + + /** The endpoint URI on the line: the string of from, to, toD... in Java, the uri attribute of those in XML. */ + static Value uri(String dsl, String line) { + if (line == null) { + return null; + } + Matcher m = ("xml".equals(dsl) ? XML_URI : JAVA_URI).matcher(line); + if (!m.find()) { + return null; + } + if ("xml".equals(dsl)) { + int group = m.group(2) != null ? 2 : 3; + return new Value(m.start(group), m.end(group), xmlDecode(m.group(group))); + } + return new Value(m.start(1), m.end(1), javaUnescape(m.group(1))); + } + + // ---- the value at the cursor ---- + + /** The string literal (Java) or attribute value (XML) the cursor is on; null when it is on none. */ + static Value valueAt(String dsl, String line, int col) { + if (line == null || col < 0) { + return null; + } + if ("xml".equals(dsl)) { + Matcher m = XML_ATTRIBUTE.matcher(line); + while (m.find()) { + int group = m.group(2) != null ? 2 : 3; + // on the attribute name or in its value, the quotes included + if (col >= m.start() && col <= m.end()) { + return new Value(m.start(group), m.end(group), xmlDecode(m.group(group))); + } + } + return null; + } + int open = -1; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (open < 0 && c == '/' && i + 1 < line.length() && line.charAt(i + 1) == '/') { + return null; + } + if (open >= 0 && c == '\\') { + i++; + } else if (c == '"') { + if (open < 0) { + open = i; + } else { + if (col >= open && col <= i + 1) { + return new Value(open + 1, i, javaUnescape(line.substring(open + 1, i))); + } + open = -1; + } + } + } + return null; + } + + /** Whether a value can be extracted to a property: something, and no placeholder already. */ + static boolean isExtractable(Value v) { + return v != null && !v.text().isBlank() && !(v.text().startsWith("{{") && v.text().endsWith("}}")); + } + + /** The line with the value replaced, written as the DSL quotes it (escaped in Java, entities in XML). */ + static String replace(String dsl, String line, Value v, String text) { + String written = "xml".equals(dsl) ? xmlEncode(text) : javaEscape(text); + return line.substring(0, v.start()) + written + line.substring(v.end()); + } + + /** + * The application.properties a property of the route file goes to: src/main/resources of a Maven or Gradle layout, + * else the file's own folder, as camel run reads it. + */ + static Path propertiesFile(Path routeFile) { + Path dir = routeFile.toAbsolutePath().getParent(); + for (Path p = dir; p != null; p = p.getParent()) { + if (p.getFileName() != null && "src".equals(p.getFileName().toString())) { + return p.resolve("main/resources/application.properties"); + } + } + return dir.resolve("application.properties"); + } + + // ---- an XML step block ---- + + /** The rows of an XML element block, its start tag on the first. */ + record Block(int start, int end, String element, int indent) { + } + + private static final Pattern START_TAG = Pattern.compile("^(\\s*)<(?:[\\w-]+:)?([\\w-]+)"); + + /** The step block whose start tag is on the row; null when the row holds none, or no step to extract. */ + static Block xmlStep(List<String> lines, int row) { + if (row < 0 || row >= lines.size()) { + return null; + } + Matcher m = START_TAG.matcher(lines.get(row)); + if (!m.find() || NOT_EXTRACTABLE.contains(m.group(2))) { + return null; + } + // must be inside a route + XmlCompletionContext c = XmlCompletionContext.at(lines, row, m.start(2) - 1); + if (c == null || !c.path().contains("route")) { + return null; + } + int depth = 0; + for (int r = row; r < lines.size(); r++) { + depth += tagBalance(lines.get(r)); + if (depth <= 0) { + return new Block(row, r, m.group(2), m.group(1).length()); + } + } + return null; + } + + /** + * The opening minus the closing tags of a line: self-closing tags, comments and processing instructions count 0. + */ + static int tagBalance(String line) { + int balance = 0; + String s = line.replaceAll("<!--.*?-->", "").replaceAll("<\\?.*?\\?>", ""); + Matcher m = Pattern.compile("<(/?)[\\w:-]+[^>]*?(/?)>").matcher(s); + while (m.find()) { + if (!m.group(1).isEmpty()) { + balance--; + } else if (m.group(2).isEmpty()) { + balance++; + } + } + return balance; + } + + /** A new XML routes file with the block as a route consuming from direct:name. */ + static String xmlRouteFile(String name, List<String> block, int indent) { + StringBuilder sb = new StringBuilder(); + sb.append("<routes xmlns=\"http://camel.apache.org/schema/xml-io\">\n"); + sb.append(xmlRoute(name, block, indent)); + sb.append("</routes>\n"); + return sb.toString(); + } + + /** A route of the block consuming from direct:name, indented as a route of a routes file. */ + static String xmlRoute(String name, List<String> block, int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(" <route id=\"").append(xmlEncode(name)).append("\">\n"); + sb.append(" <from uri=\"direct:").append(xmlEncode(name)).append("\"/>\n"); + for (String line : block) { + String body + = line.length() >= indent && line.substring(0, indent).isBlank() ? line.substring(indent) : line.strip(); + sb.append(body.isBlank() ? "" : " " + body).append('\n'); + } + sb.append(" </route>\n"); + return sb.toString(); + } + + /** An existing XML routes file with the route added before its end tag; null when it has no routes end tag. */ + static String addXmlRoute(String existing, String route) { + int end = existing.lastIndexOf("</routes>"); + if (end < 0) { + return null; + } + return existing.substring(0, end) + route + existing.substring(end); + } + + /** The lines with the block replaced by a to of direct:name, at the block's indent. */ + static List<String> replaceWithTo(List<String> lines, Block block, String name) { + List<String> answer = new ArrayList<>(lines.subList(0, block.start())); + answer.add(" ".repeat(block.indent()) + "<to uri=\"direct:" + xmlEncode(name) + "\"/>"); + answer.addAll(lines.subList(block.end() + 1, lines.size())); + return answer; + } + + // ---- quoting ---- + + static String xmlDecode(String s) { + return s.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'") + .replace("&", "&"); + } + + static String xmlEncode(String s) { + return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); + } + + static String javaUnescape(String s) { + return s.replace("\\\"", "\"").replace("\\\\", "\\"); + } + Review Comment: 💡 **Medium — Correctness:** `javaUnescape` only handles `\"` and `\\`, but Java string literals can contain `\n`, `\t`, `\r`, `\'`, `\uXXXX`, and octal escapes. If a user extracts a value like `"Hello\nWorld"` to a property, the property value will contain the literal characters `\n` instead of a newline. In practice, most Camel route strings are URIs or simple values without these escapes, so this may be acceptable as a known limitation. But it's worth a comment if intentional. ########## dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteRefactorings.java: ########## @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.tui; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The refactorings of the source editor (Ctrl+R) for Java and XML routes (CAMEL-25256), as {@link SourceRefactorings} + * has them for YAML: the endpoint URI of a line, the string literal (Java) or attribute value (XML) at the cursor, and + * the element block of an XML step, with what replaces them. + */ +final class RouteRefactorings { + + /** + * A quoted value on a line: where its text starts and ends (inside the quotes), and the text as the route means it + * (escapes and entities undone). + */ + record Value(int start, int end, String text) { + } + + /** The DSL methods (Java) and elements (XML) whose argument or uri attribute is an endpoint URI. */ + private static final String ENDPOINT_CALLS = "from|to|toD|wireTap|enrich|pollEnrich|poll"; + private static final Pattern JAVA_URI = Pattern.compile( + "\\b(?:" + ENDPOINT_CALLS + ")\\s*\\(\\s*\"((?:[^\"\\\\]|\\\\.)*)\""); + private static final Pattern XML_URI = Pattern.compile( + "<(?:[\\w-]+:)?(?:" + ENDPOINT_CALLS + ")\\b[^>]*?\\suri\\s*=\\s*(\"([^\"]*)\"|'([^']*)')"); + private static final Pattern XML_ATTRIBUTE = Pattern.compile("[\\w:.-]+\\s*=\\s*(\"([^\"]*)\"|'([^']*)')"); + + /** Elements that are no step to extract: the route, its input, and the parts of the steps that hold them. */ + private static final Set<String> NOT_EXTRACTABLE = Set.of( + "routes", "route", "camel", "beans", "from", "when", "otherwise", "doCatch", "doFinally", "onFallback", + "expression", "simple", "constant", "header", "exchangeProperty", "variable", "jsonpath", "xpath", + "groovy", "language", "method", "tokenize", "jq", "ref", "datasonnet", "spel", "mvel", "ognl", "js", + "python", "java", "wasm", "xquery", "xtokenize", "hl7terser", "jactl", "csimple", "joor"); + + private RouteRefactorings() { + } + + // ---- the endpoint URI of a line ---- + + /** The endpoint URI on the line: the string of from, to, toD... in Java, the uri attribute of those in XML. */ + static Value uri(String dsl, String line) { + if (line == null) { + return null; + } + Matcher m = ("xml".equals(dsl) ? XML_URI : JAVA_URI).matcher(line); + if (!m.find()) { + return null; + } + if ("xml".equals(dsl)) { + int group = m.group(2) != null ? 2 : 3; + return new Value(m.start(group), m.end(group), xmlDecode(m.group(group))); + } + return new Value(m.start(1), m.end(1), javaUnescape(m.group(1))); + } + + // ---- the value at the cursor ---- + + /** The string literal (Java) or attribute value (XML) the cursor is on; null when it is on none. */ + static Value valueAt(String dsl, String line, int col) { + if (line == null || col < 0) { + return null; + } + if ("xml".equals(dsl)) { + Matcher m = XML_ATTRIBUTE.matcher(line); + while (m.find()) { + int group = m.group(2) != null ? 2 : 3; + // on the attribute name or in its value, the quotes included + if (col >= m.start() && col <= m.end()) { + return new Value(m.start(group), m.end(group), xmlDecode(m.group(group))); + } + } + return null; + } + int open = -1; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (open < 0 && c == '/' && i + 1 < line.length() && line.charAt(i + 1) == '/') { + return null; + } + if (open >= 0 && c == '\\') { + i++; + } else if (c == '"') { + if (open < 0) { + open = i; + } else { + if (col >= open && col <= i + 1) { + return new Value(open + 1, i, javaUnescape(line.substring(open + 1, i))); + } + open = -1; + } + } + } + return null; + } + + /** Whether a value can be extracted to a property: something, and no placeholder already. */ + static boolean isExtractable(Value v) { + return v != null && !v.text().isBlank() && !(v.text().startsWith("{{") && v.text().endsWith("}}")); + } + + /** The line with the value replaced, written as the DSL quotes it (escaped in Java, entities in XML). */ + static String replace(String dsl, String line, Value v, String text) { + String written = "xml".equals(dsl) ? xmlEncode(text) : javaEscape(text); + return line.substring(0, v.start()) + written + line.substring(v.end()); + } + + /** + * The application.properties a property of the route file goes to: src/main/resources of a Maven or Gradle layout, + * else the file's own folder, as camel run reads it. + */ + static Path propertiesFile(Path routeFile) { + Path dir = routeFile.toAbsolutePath().getParent(); + for (Path p = dir; p != null; p = p.getParent()) { + if (p.getFileName() != null && "src".equals(p.getFileName().toString())) { + return p.resolve("main/resources/application.properties"); + } + } + return dir.resolve("application.properties"); + } + + // ---- an XML step block ---- + + /** The rows of an XML element block, its start tag on the first. */ + record Block(int start, int end, String element, int indent) { + } + + private static final Pattern START_TAG = Pattern.compile("^(\\s*)<(?:[\\w-]+:)?([\\w-]+)"); + + /** The step block whose start tag is on the row; null when the row holds none, or no step to extract. */ + static Block xmlStep(List<String> lines, int row) { + if (row < 0 || row >= lines.size()) { + return null; + } + Matcher m = START_TAG.matcher(lines.get(row)); + if (!m.find() || NOT_EXTRACTABLE.contains(m.group(2))) { + return null; + } + // must be inside a route + XmlCompletionContext c = XmlCompletionContext.at(lines, row, m.start(2) - 1); + if (c == null || !c.path().contains("route")) { + return null; + } + int depth = 0; + for (int r = row; r < lines.size(); r++) { + depth += tagBalance(lines.get(r)); + if (depth <= 0) { + return new Block(row, r, m.group(2), m.group(1).length()); + } + } + return null; + } + + /** + * The opening minus the closing tags of a line: self-closing tags, comments and processing instructions count 0. + */ + static int tagBalance(String line) { + int balance = 0; + String s = line.replaceAll("<!--.*?-->", "").replaceAll("<\\?.*?\\?>", ""); + Matcher m = Pattern.compile("<(/?)[\\w:-]+[^>]*?(/?)>").matcher(s); + while (m.find()) { Review Comment: 💡 **Medium — Correctness edge case:** The comment-stripping regex `<!--.*?-->` uses `.*?` which doesn't match across lines. If an XML comment spans multiple lines (e.g., `<!--` on one line, `-->` on a later line), the tags inside the commented-out block will be counted, potentially producing an incorrect tag balance and a wrong `Block` boundary. This is probably rare in practice (most XML comments in route files are single-line), but multi-line comments could cause `xmlStep` to return incorrect block boundaries, leading to an extraction that cuts in the middle of a construct. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
