[ 
https://issues.apache.org/jira/browse/GROOVY-12018?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18081878#comment-18081878
 ] 

ASF GitHub Bot commented on GROOVY-12018:
-----------------------------------------

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.





> allow format to be case insensitive for groovysh: /print -s FORMAT 
> -------------------------------------------------------------------
>
>                 Key: GROOVY-12018
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12018
>             Project: Groovy
>          Issue Type: Improvement
>          Components: Groovysh
>            Reporter: Paul King
>            Priority: Major
>




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to