Copilot commented on code in PR #2543:
URL: https://github.com/apache/groovy/pull/2543#discussion_r3263070479


##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ *  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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+    private static final List<String> STYLE_KEYS = [Printer.STYLE, 
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+    private static final Pattern SYNTAX_HEADER = 
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+    private static final Pattern INCLUDE_LINE = 
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+    private final ConfigurationPath configPath
+    private Map<String, String> nameByLower // cached lowercase -> actual; 
built lazily
+
+    GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+        super(engine, configPath)
+        this.configPath = configPath
+    }
+
+    @Override
+    void println(Map<String, Object> options, Object object) {
+        super.println(normalizeStyles(options), object)
+    }
+
+    @Override
+    protected void highlightAndPrint(Map<String, Object> options, Throwable 
exception) {
+        super.highlightAndPrint(normalizeStyles(options), exception)
+    }
+
+    /**
+     * Returns a copy of {@code options} with style-name values resolved to the
+     * actual configured syntax name (case-insensitively), or the original map
+     * untouched when there is nothing to change (so an immutable or shared
+     * caller map is not mutated). Failsafe: any problem returns {@code 
options}
+     * unchanged.
+     */
+    private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+        if (options == null || options.isEmpty()) {
+            return options
+        }
+        Map<String, String> names
+        try {
+            names = syntaxNames()
+        } catch (Exception ignored) {
+            return options
+        }
+        if (names.isEmpty()) {
+            return options
+        }
+        Map<String, Object> copy = null
+        for (String key : STYLE_KEYS) {
+            Object value = options.get(key)
+            if (value instanceof CharSequence && (value as 
CharSequence).length() > 0) {
+                String requested = value.toString()
+                String resolved = resolveStyle(requested, names)
+                if (resolved != requested) {
+                    if (copy == null) {
+                        copy = new LinkedHashMap<String, Object>(options)
+                    }
+                    copy.put(key, resolved)
+                }
+            }
+        }
+        copy != null ? copy : options
+    }
+
+    /**
+     * Pure resolution (package-private for testing): an exact match or an
+     * unknown name is returned unchanged; otherwise the actual syntax name for
+     * a case-insensitive match is returned.
+     *
+     * @param requested the user-supplied style name
+     * @param nameByLower map of lower-cased syntax name to its actual casing
+     * @return the name JLine should be given
+     */
+    static String resolveStyle(String requested, Map<String, String> 
nameByLower) {
+        if (requested == null || requested.isEmpty()) {
+            return requested
+        }
+        if (nameByLower.containsValue(requested)) {
+            return requested // exact match: leave as-is (also covers 
user-config names)
+        }
+        String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+        actual != null ? actual : requested
+    }
+
+    /** lowercase-name -&gt; actual syntax name across the resolved jnanorc 
and its includes; cached. */
+    private synchronized Map<String, String> syntaxNames() {
+        if (nameByLower != null) {
+            return nameByLower

Review Comment:
   The syntax-name cache is never invalidated. `Main` registers 
`printer::refresh` with the system highlighter refresh path, but 
`GroovyPrinter` inherits `DefaultPrinter.refresh()` without clearing 
`nameByLower`, so after a user changes/copies nanorc files and refreshes, 
case-insensitive style resolution still uses the old names until groovysh is 
restarted.



##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ *  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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+    private static final List<String> STYLE_KEYS = [Printer.STYLE, 
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+    private static final Pattern SYNTAX_HEADER = 
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+    private static final Pattern INCLUDE_LINE = 
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+    private final ConfigurationPath configPath
+    private Map<String, String> nameByLower // cached lowercase -> actual; 
built lazily
+
+    GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+        super(engine, configPath)
+        this.configPath = configPath
+    }
+
+    @Override
+    void println(Map<String, Object> options, Object object) {
+        super.println(normalizeStyles(options), object)
+    }
+
+    @Override
+    protected void highlightAndPrint(Map<String, Object> options, Throwable 
exception) {
+        super.highlightAndPrint(normalizeStyles(options), exception)
+    }
+
+    /**
+     * Returns a copy of {@code options} with style-name values resolved to the
+     * actual configured syntax name (case-insensitively), or the original map
+     * untouched when there is nothing to change (so an immutable or shared
+     * caller map is not mutated). Failsafe: any problem returns {@code 
options}
+     * unchanged.
+     */
+    private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+        if (options == null || options.isEmpty()) {
+            return options
+        }
+        Map<String, String> names
+        try {
+            names = syntaxNames()
+        } catch (Exception ignored) {
+            return options
+        }
+        if (names.isEmpty()) {
+            return options
+        }
+        Map<String, Object> copy = null
+        for (String key : STYLE_KEYS) {
+            Object value = options.get(key)
+            if (value instanceof CharSequence && (value as 
CharSequence).length() > 0) {
+                String requested = value.toString()
+                String resolved = resolveStyle(requested, names)
+                if (resolved != requested) {
+                    if (copy == null) {
+                        copy = new LinkedHashMap<String, Object>(options)
+                    }
+                    copy.put(key, resolved)
+                }
+            }
+        }
+        copy != null ? copy : options
+    }
+
+    /**
+     * Pure resolution (package-private for testing): an exact match or an
+     * unknown name is returned unchanged; otherwise the actual syntax name for
+     * a case-insensitive match is returned.
+     *
+     * @param requested the user-supplied style name
+     * @param nameByLower map of lower-cased syntax name to its actual casing
+     * @return the name JLine should be given
+     */
+    static String resolveStyle(String requested, Map<String, String> 
nameByLower) {
+        if (requested == null || requested.isEmpty()) {
+            return requested
+        }
+        if (nameByLower.containsValue(requested)) {
+            return requested // exact match: leave as-is (also covers 
user-config names)
+        }
+        String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+        actual != null ? actual : requested
+    }
+
+    /** lowercase-name -&gt; actual syntax name across the resolved jnanorc 
and its includes; cached. */
+    private synchronized Map<String, String> syntaxNames() {
+        if (nameByLower != null) {
+            return nameByLower
+        }
+        Map<String, String> map = new LinkedHashMap<>()
+        Path jnanorc = configPath?.getConfig('jnanorc')
+        if (jnanorc != null && Files.isReadable(jnanorc)) {
+            collectSyntaxNames(jnanorc, map)
+        }
+        nameByLower = map
+        return map
+    }
+
+    /**
+     * Scans the jnanorc itself and every file it {@code include}s for
+     * {@code syntax "NAME"} headers. Per-file and per-include failsafe.
+     */
+    static void collectSyntaxNames(Path jnanorc, Map<String, String> into) {
+        scanForSyntax(jnanorc, into) // jnanorc may declare syntaxes directly
+        String text
+        try {
+            text = new String(Files.readAllBytes(jnanorc))

Review Comment:
   This decodes nanorc files with the JVM default charset even though 
repository text files are declared UTF-8 in `.editorconfig:18`. On Java 17 
runtimes with a non-UTF-8 default charset, non-ASCII include paths or syntax 
names can be misread; use an explicit UTF-8 charset here.



##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ *  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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+    private static final List<String> STYLE_KEYS = [Printer.STYLE, 
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+    private static final Pattern SYNTAX_HEADER = 
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+    private static final Pattern INCLUDE_LINE = 
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+    private final ConfigurationPath configPath
+    private Map<String, String> nameByLower // cached lowercase -> actual; 
built lazily
+
+    GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+        super(engine, configPath)
+        this.configPath = configPath
+    }
+
+    @Override
+    void println(Map<String, Object> options, Object object) {
+        super.println(normalizeStyles(options), object)
+    }
+
+    @Override
+    protected void highlightAndPrint(Map<String, Object> options, Throwable 
exception) {
+        super.highlightAndPrint(normalizeStyles(options), exception)
+    }
+
+    /**
+     * Returns a copy of {@code options} with style-name values resolved to the
+     * actual configured syntax name (case-insensitively), or the original map
+     * untouched when there is nothing to change (so an immutable or shared
+     * caller map is not mutated). Failsafe: any problem returns {@code 
options}
+     * unchanged.
+     */
+    private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+        if (options == null || options.isEmpty()) {
+            return options
+        }
+        Map<String, String> names
+        try {
+            names = syntaxNames()
+        } catch (Exception ignored) {
+            return options
+        }
+        if (names.isEmpty()) {
+            return options
+        }
+        Map<String, Object> copy = null
+        for (String key : STYLE_KEYS) {
+            Object value = options.get(key)
+            if (value instanceof CharSequence && (value as 
CharSequence).length() > 0) {
+                String requested = value.toString()
+                String resolved = resolveStyle(requested, names)
+                if (resolved != requested) {
+                    if (copy == null) {
+                        copy = new LinkedHashMap<String, Object>(options)
+                    }
+                    copy.put(key, resolved)
+                }
+            }
+        }
+        copy != null ? copy : options
+    }
+
+    /**
+     * Pure resolution (package-private for testing): an exact match or an
+     * unknown name is returned unchanged; otherwise the actual syntax name for
+     * a case-insensitive match is returned.
+     *
+     * @param requested the user-supplied style name
+     * @param nameByLower map of lower-cased syntax name to its actual casing
+     * @return the name JLine should be given
+     */
+    static String resolveStyle(String requested, Map<String, String> 
nameByLower) {
+        if (requested == null || requested.isEmpty()) {
+            return requested
+        }
+        if (nameByLower.containsValue(requested)) {
+            return requested // exact match: leave as-is (also covers 
user-config names)
+        }
+        String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+        actual != null ? actual : requested
+    }
+
+    /** lowercase-name -&gt; actual syntax name across the resolved jnanorc 
and its includes; cached. */
+    private synchronized Map<String, String> syntaxNames() {
+        if (nameByLower != null) {
+            return nameByLower
+        }
+        Map<String, String> map = new LinkedHashMap<>()
+        Path jnanorc = configPath?.getConfig('jnanorc')
+        if (jnanorc != null && Files.isReadable(jnanorc)) {
+            collectSyntaxNames(jnanorc, map)
+        }
+        nameByLower = map
+        return map
+    }
+
+    /**
+     * Scans the jnanorc itself and every file it {@code include}s for
+     * {@code syntax "NAME"} headers. Per-file and per-include failsafe.
+     */
+    static void collectSyntaxNames(Path jnanorc, Map<String, String> into) {

Review Comment:
   This test/support helper is also public by default in Groovy. To avoid 
adding unnecessary public API surface, follow AGENTS.md:98's narrowest-scope 
guidance and make it package-scoped if it only needs to be called from 
same-package tests.



##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ *  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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+    private static final List<String> STYLE_KEYS = [Printer.STYLE, 
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+    private static final Pattern SYNTAX_HEADER = 
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+    private static final Pattern INCLUDE_LINE = 
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+    private final ConfigurationPath configPath
+    private Map<String, String> nameByLower // cached lowercase -> actual; 
built lazily
+
+    GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+        super(engine, configPath)
+        this.configPath = configPath
+    }
+
+    @Override
+    void println(Map<String, Object> options, Object object) {
+        super.println(normalizeStyles(options), object)
+    }
+
+    @Override
+    protected void highlightAndPrint(Map<String, Object> options, Throwable 
exception) {
+        super.highlightAndPrint(normalizeStyles(options), exception)
+    }
+
+    /**
+     * Returns a copy of {@code options} with style-name values resolved to the
+     * actual configured syntax name (case-insensitively), or the original map
+     * untouched when there is nothing to change (so an immutable or shared
+     * caller map is not mutated). Failsafe: any problem returns {@code 
options}
+     * unchanged.
+     */
+    private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+        if (options == null || options.isEmpty()) {
+            return options
+        }
+        Map<String, String> names
+        try {
+            names = syntaxNames()
+        } catch (Exception ignored) {
+            return options
+        }
+        if (names.isEmpty()) {
+            return options
+        }
+        Map<String, Object> copy = null
+        for (String key : STYLE_KEYS) {
+            Object value = options.get(key)
+            if (value instanceof CharSequence && (value as 
CharSequence).length() > 0) {
+                String requested = value.toString()
+                String resolved = resolveStyle(requested, names)
+                if (resolved != requested) {
+                    if (copy == null) {
+                        copy = new LinkedHashMap<String, Object>(options)
+                    }
+                    copy.put(key, resolved)
+                }
+            }
+        }
+        copy != null ? copy : options
+    }
+
+    /**
+     * Pure resolution (package-private for testing): an exact match or an
+     * unknown name is returned unchanged; otherwise the actual syntax name for
+     * a case-insensitive match is returned.
+     *
+     * @param requested the user-supplied style name
+     * @param nameByLower map of lower-cased syntax name to its actual casing
+     * @return the name JLine should be given
+     */
+    static String resolveStyle(String requested, Map<String, String> 
nameByLower) {

Review Comment:
   In Groovy, a method without an explicit visibility modifier is public, so 
this helper becomes new public API despite the comment saying it is 
package-private. AGENTS.md:98 asks to prefer the narrowest scope because public 
API is hard to remove; mark test-only helpers package-scoped instead of 
exposing them.



##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ *  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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+    private static final List<String> STYLE_KEYS = [Printer.STYLE, 
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+    private static final Pattern SYNTAX_HEADER = 
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+    private static final Pattern INCLUDE_LINE = 
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+    private final ConfigurationPath configPath
+    private Map<String, String> nameByLower // cached lowercase -> actual; 
built lazily
+
+    GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+        super(engine, configPath)
+        this.configPath = configPath
+    }
+
+    @Override
+    void println(Map<String, Object> options, Object object) {
+        super.println(normalizeStyles(options), object)
+    }
+
+    @Override
+    protected void highlightAndPrint(Map<String, Object> options, Throwable 
exception) {
+        super.highlightAndPrint(normalizeStyles(options), exception)
+    }
+
+    /**
+     * Returns a copy of {@code options} with style-name values resolved to the
+     * actual configured syntax name (case-insensitively), or the original map
+     * untouched when there is nothing to change (so an immutable or shared
+     * caller map is not mutated). Failsafe: any problem returns {@code 
options}
+     * unchanged.
+     */
+    private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+        if (options == null || options.isEmpty()) {
+            return options
+        }
+        Map<String, String> names
+        try {
+            names = syntaxNames()
+        } catch (Exception ignored) {
+            return options
+        }
+        if (names.isEmpty()) {
+            return options
+        }
+        Map<String, Object> copy = null
+        for (String key : STYLE_KEYS) {
+            Object value = options.get(key)
+            if (value instanceof CharSequence && (value as 
CharSequence).length() > 0) {
+                String requested = value.toString()
+                String resolved = resolveStyle(requested, names)
+                if (resolved != requested) {
+                    if (copy == null) {
+                        copy = new LinkedHashMap<String, Object>(options)
+                    }
+                    copy.put(key, resolved)
+                }
+            }
+        }
+        copy != null ? copy : options
+    }
+
+    /**
+     * Pure resolution (package-private for testing): an exact match or an
+     * unknown name is returned unchanged; otherwise the actual syntax name for
+     * a case-insensitive match is returned.
+     *
+     * @param requested the user-supplied style name
+     * @param nameByLower map of lower-cased syntax name to its actual casing
+     * @return the name JLine should be given
+     */
+    static String resolveStyle(String requested, Map<String, String> 
nameByLower) {
+        if (requested == null || requested.isEmpty()) {
+            return requested
+        }
+        if (nameByLower.containsValue(requested)) {
+            return requested // exact match: leave as-is (also covers 
user-config names)
+        }
+        String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+        actual != null ? actual : requested
+    }
+
+    /** lowercase-name -&gt; actual syntax name across the resolved jnanorc 
and its includes; cached. */
+    private synchronized Map<String, String> syntaxNames() {
+        if (nameByLower != null) {
+            return nameByLower
+        }
+        Map<String, String> map = new LinkedHashMap<>()
+        Path jnanorc = configPath?.getConfig('jnanorc')
+        if (jnanorc != null && Files.isReadable(jnanorc)) {
+            collectSyntaxNames(jnanorc, map)
+        }
+        nameByLower = map
+        return map
+    }
+
+    /**
+     * Scans the jnanorc itself and every file it {@code include}s for
+     * {@code syntax "NAME"} headers. Per-file and per-include failsafe.
+     */
+    static void collectSyntaxNames(Path jnanorc, Map<String, String> into) {
+        scanForSyntax(jnanorc, into) // jnanorc may declare syntaxes directly
+        String text
+        try {
+            text = new String(Files.readAllBytes(jnanorc))
+        } catch (Exception ignored) {
+            return
+        }
+        Path dir = jnanorc.toAbsolutePath().parent
+        Matcher m = INCLUDE_LINE.matcher(text)
+        while (m.find()) {
+            String glob = m.group(1)
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, 
glob)) {
+                for (Path p : stream) {
+                    scanForSyntax(p, into) // per-file failsafe inside
+                }
+            } catch (Exception ignored) {
+                // one bad include directive must not stop the others
+            }
+        }
+    }
+
+    /**
+     * Per-file failsafe: a single malformed/unreadable nanorc never breaks
+     * discovery of the rest. First spelling of a name wins (stable order).
+     */
+    private static void scanForSyntax(Path file, Map<String, String> into) {
+        try {
+            if (file == null || !Files.isReadable(file)) {
+                return
+            }
+            String content = new String(Files.readAllBytes(file))

Review Comment:
   This second read also uses the platform default charset, while repository 
text files are UTF-8 per `.editorconfig:18`. A non-UTF-8 Java 17 runtime can 
corrupt non-ASCII syntax names before lower-casing/matching them; decode with 
an explicit UTF-8 charset.



##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ *  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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+    private static final List<String> STYLE_KEYS = [Printer.STYLE, 
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+    private static final Pattern SYNTAX_HEADER = 
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+    private static final Pattern INCLUDE_LINE = 
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+    private final ConfigurationPath configPath
+    private Map<String, String> nameByLower // cached lowercase -> actual; 
built lazily
+
+    GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+        super(engine, configPath)
+        this.configPath = configPath
+    }
+
+    @Override
+    void println(Map<String, Object> options, Object object) {
+        super.println(normalizeStyles(options), object)
+    }
+
+    @Override
+    protected void highlightAndPrint(Map<String, Object> options, Throwable 
exception) {
+        super.highlightAndPrint(normalizeStyles(options), exception)
+    }
+
+    /**
+     * Returns a copy of {@code options} with style-name values resolved to the
+     * actual configured syntax name (case-insensitively), or the original map
+     * untouched when there is nothing to change (so an immutable or shared
+     * caller map is not mutated). Failsafe: any problem returns {@code 
options}
+     * unchanged.
+     */
+    private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+        if (options == null || options.isEmpty()) {
+            return options
+        }
+        Map<String, String> names
+        try {
+            names = syntaxNames()
+        } catch (Exception ignored) {
+            return options
+        }
+        if (names.isEmpty()) {
+            return options
+        }
+        Map<String, Object> copy = null
+        for (String key : STYLE_KEYS) {
+            Object value = options.get(key)
+            if (value instanceof CharSequence && (value as 
CharSequence).length() > 0) {
+                String requested = value.toString()
+                String resolved = resolveStyle(requested, names)
+                if (resolved != requested) {
+                    if (copy == null) {
+                        copy = new LinkedHashMap<String, Object>(options)
+                    }
+                    copy.put(key, resolved)
+                }
+            }
+        }
+        copy != null ? copy : options
+    }
+
+    /**
+     * Pure resolution (package-private for testing): an exact match or an
+     * unknown name is returned unchanged; otherwise the actual syntax name for
+     * a case-insensitive match is returned.
+     *
+     * @param requested the user-supplied style name
+     * @param nameByLower map of lower-cased syntax name to its actual casing
+     * @return the name JLine should be given
+     */
+    static String resolveStyle(String requested, Map<String, String> 
nameByLower) {
+        if (requested == null || requested.isEmpty()) {
+            return requested
+        }
+        if (nameByLower.containsValue(requested)) {
+            return requested // exact match: leave as-is (also covers 
user-config names)
+        }
+        String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+        actual != null ? actual : requested
+    }
+
+    /** lowercase-name -&gt; actual syntax name across the resolved jnanorc 
and its includes; cached. */
+    private synchronized Map<String, String> syntaxNames() {
+        if (nameByLower != null) {
+            return nameByLower
+        }
+        Map<String, String> map = new LinkedHashMap<>()
+        Path jnanorc = configPath?.getConfig('jnanorc')
+        if (jnanorc != null && Files.isReadable(jnanorc)) {
+            collectSyntaxNames(jnanorc, map)
+        }
+        nameByLower = map
+        return map
+    }
+
+    /**
+     * Scans the jnanorc itself and every file it {@code include}s for
+     * {@code syntax "NAME"} headers. Per-file and per-include failsafe.
+     */
+    static void collectSyntaxNames(Path jnanorc, Map<String, String> into) {
+        scanForSyntax(jnanorc, into) // jnanorc may declare syntaxes directly
+        String text
+        try {
+            text = new String(Files.readAllBytes(jnanorc))
+        } catch (Exception ignored) {
+            return
+        }
+        Path dir = jnanorc.toAbsolutePath().parent
+        Matcher m = INCLUDE_LINE.matcher(text)
+        while (m.find()) {
+            String glob = m.group(1)
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, 
glob)) {
+                for (Path p : stream) {
+                    scanForSyntax(p, into) // per-file failsafe inside
+                }
+            } catch (Exception ignored) {
+                // one bad include directive must not stop the others
+            }
+        }
+    }
+
+    /**
+     * Per-file failsafe: a single malformed/unreadable nanorc never breaks
+     * discovery of the rest. First spelling of a name wins (stable order).
+     */
+    private static void scanForSyntax(Path file, Map<String, String> into) {
+        try {
+            if (file == null || !Files.isReadable(file)) {
+                return
+            }
+            String content = new String(Files.readAllBytes(file))
+            Matcher m = SYNTAX_HEADER.matcher(content)
+            while (m.find()) {
+                String name = m.group(1)
+                into.putIfAbsent(name.toLowerCase(Locale.ROOT), name)

Review Comment:
   Collapsing syntax names with `putIfAbsent` loses later names that differ 
only by case. If a user has both `JSON` and `json`, the later exact name is no 
longer in the map, so `resolveStyle('json', ...)` rewrites it to the first 
spelling instead of preserving the exact match. Track exact names/ambiguity 
separately, or avoid rewriting non-unique case-insensitive matches.



-- 
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]


Reply via email to