001package gudusoft.gsqlparser.pp2.layout.rules; 002 003import gudusoft.gsqlparser.ETokenType; 004import gudusoft.gsqlparser.pp.para.GFmtOpt; 005import gudusoft.gsqlparser.pp.para.styleenums.TCaseOption; 006import gudusoft.gsqlparser.pp2.layout.LayoutContext; 007import gudusoft.gsqlparser.pp2.layout.LayoutPriorities; 008import gudusoft.gsqlparser.pp2.layout.LayoutRule; 009import gudusoft.gsqlparser.pp2.token.Pp2Token; 010import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 011 012import java.util.Locale; 013 014/** 015 * Applies keyword / identifier / function-name case modification, reusing the 016 * existing {@link GFmtOpt#caseKeywords}, {@link GFmtOpt#caseIdentifier}, and 017 * {@link GFmtOpt#caseFuncname} options. 018 * 019 * <p>This is a <i>text</i> rule, not a whitespace rule: it requests a per-token 020 * render-time text override via {@code LayoutContext.requestText}. The wrapped 021 * {@link gudusoft.gsqlparser.TSourceToken} is never mutated. 022 * 023 * <ul> 024 * <li>{@code ttkeyword} → {@code caseKeywords} (default upper-case).</li> 025 * <li>{@code ttidentifier} immediately followed by {@code (} → a function name 026 * → {@code caseFuncname}; otherwise an identifier → {@code caseIdentifier}.</li> 027 * </ul> 028 * 029 * <p>Quoted identifiers, string/numeric literals, comments, and punctuation are 030 * never re-cased — only bare keywords and identifiers. Priority 031 * {@link LayoutPriorities#CASE_MODIFICATION} (highest; text is orthogonal to 032 * whitespace). Iterative; read-only over tokens. 033 * 034 * <p>Plan reference: §7.3/S29, §7.4/S29. 035 */ 036public final class CaseModificationRules implements LayoutRule { 037 038 @Override 039 public int priority() { return LayoutPriorities.CASE_MODIFICATION; } 040 041 @Override 042 public String name() { return "CaseModificationRules"; } 043 044 @Override 045 public void apply(LayoutContext context) { 046 GFmtOpt opt = context.getOptions().toGFmtOpt(); 047 Pp2TokenStream stream = context.getStream(); 048 int n = stream.size(); 049 for (int i = 0; i < n; i++) { 050 Pp2Token t = stream.get(i); 051 ETokenType type = t.getSourceToken().tokentype; 052 String text = t.getText(); 053 if (text == null || text.isEmpty()) continue; 054 055 TCaseOption option; 056 if (type == ETokenType.ttkeyword) { 057 option = opt.caseKeywords; 058 } else if (type == ETokenType.ttidentifier) { 059 // A delimited identifier ("X" / [X] / `X`) is case-sensitive and 060 // must never be re-cased, even if tokenised as ttidentifier. 061 if (isDelimitedIdentifier(text)) continue; 062 option = isFunctionName(stream, i) ? opt.caseFuncname : opt.caseIdentifier; 063 } else { 064 continue; // literals, quoted ids, punctuation, comments: never re-cased 065 } 066 if (option == null || option == TCaseOption.CoNoChange) continue; 067 String recased = applyCase(text, option); 068 if (!recased.equals(text)) { 069 context.requestText(i, recased); 070 } 071 } 072 } 073 074 /** True if the identifier text begins with a quote/bracket/backtick delimiter. */ 075 private static boolean isDelimitedIdentifier(String text) { 076 char c = text.charAt(0); 077 return c == '"' || c == '[' || c == '`'; 078 } 079 080 /** An identifier is a function name when its next solid token is an open paren. */ 081 private static boolean isFunctionName(Pp2TokenStream stream, int i) { 082 for (int j = i + 1; j < stream.size(); j++) { 083 ETokenType type = stream.get(j).getSourceToken().tokentype; 084 if (type == ETokenType.ttsimplecomment 085 || type == ETokenType.ttbracketedcomment 086 || type == ETokenType.ttCPPComment) { 087 continue; 088 } 089 return type == ETokenType.ttleftparenthesis; 090 } 091 return false; 092 } 093 094 private static String applyCase(String text, TCaseOption option) { 095 switch (option) { 096 case CoUppercase: return text.toUpperCase(Locale.ROOT); 097 case CoLowercase: return text.toLowerCase(Locale.ROOT); 098 case CoInitCap: return initCap(text); 099 case CoNoChange: 100 default: return text; 101 } 102 } 103 104 /** Upper-case the first letter, lower-case the rest (per word is overkill for a single token). */ 105 private static String initCap(String text) { 106 if (text.isEmpty()) return text; 107 return Character.toUpperCase(text.charAt(0)) 108 + text.substring(1).toLowerCase(Locale.ROOT); 109 } 110}