diff --git a/CaseConversion.sublime-settings b/CaseConversion.sublime-settings index a67ff97..18369a5 100644 --- a/CaseConversion.sublime-settings +++ b/CaseConversion.sublime-settings @@ -62,5 +62,18 @@ "GUI", "UI", "ID" - ] + ], + + // Customize toggle cases as key: value pairs. Key is starting string and its corresponding value the target string. + // Reminder: A dictionary should only contain unique keys + // Possiblie value: + // 'lower','upper', 'capital', 'snake', 'screaming_snake', 'camel', 'pascal', 'dot', 'dash', 'slash', 'backslash', + // 'title', 'separate_words' + + "toggle_cases": + { + "pascal": "snake", + "lower": "camel", + "camel": "pascal" + } } diff --git a/case_conversion.py b/case_conversion.py index aa2f011..a29a9ce 100644 --- a/case_conversion.py +++ b/case_conversion.py @@ -1,143 +1,363 @@ -import sublime -import sublime_plugin import re import sys PYTHON = sys.version_info[0] - if 3 == PYTHON: - # Python 3 and ST3 - from . import case_parse -else: - # Python 2 and ST2 - import case_parse - - -SETTINGS_FILE = "CaseConversion.sublime-settings" - - -def to_snake_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return '_'.join([w.lower() for w in words]) - - -def to_screaming_snake_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return '_'.join([w.upper() for w in words]) - - -def to_pascal_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return ''.join(words) - - -def to_camel_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - words[0] = words[0].lower() - return ''.join(words) - - -def to_dot_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return '.'.join([w.lower() for w in words]) - - -def to_dash_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return '-'.join([w.lower() for w in words]) - - -def to_slash(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms, True) - return '/'.join(words) - -def to_backslash(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms, True) - return '\\'.join(words) - - -def to_separate_words(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms, True) - return ' '.join(words) - - -def toggle_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - if case == 'pascal' and not sep: - return to_snake_case(text, detectAcronyms, acronyms) - elif case == 'lower' and sep == '_': - return to_camel_case(text, detectAcronyms, acronyms) - elif case == 'camel' and not sep: - return to_pascal_case(text, detectAcronyms, acronyms) - else: - return text - - -def run_on_selections(view, edit, func): - settings = sublime.load_settings(SETTINGS_FILE) - detectAcronyms = settings.get("detect_acronyms", True) - useList = settings.get("use_acronyms_list", True) - if useList: - acronyms = settings.get("acronyms", []) - else: - acronyms = False - - for s in view.sel(): - region = s if s else view.word(s) - - text = view.substr(region) - # Preserve leading and trailing whitespace - leading = text[:len(text)-len(text.lstrip())] - trailing = text[len(text.rstrip()):] - new_text = leading + func(text.strip(), detectAcronyms, acronyms) + trailing - if new_text != text: - view.replace(edit, region, new_text) - - -class ToggleSnakeCamelPascalCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, toggle_case) - - -class ConvertToSnakeCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_snake_case) - - -class ConvertToScreamingSnakeCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_screaming_snake_case) - - -class ConvertToCamel(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_camel_case) - - -class ConvertToPascal(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_pascal_case) - - -class ConvertToDot(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_dot_case) - - -class ConvertToDash(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_dash_case) - - -class ConvertToSeparateWords(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_separate_words) - - -class ConvertToSlash(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_slash ) - -class ConvertToBackSlash(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_backslash ) + xrange = range + + +""" +Parses a variable into a list of words. + +Also returns the case type, which can be one of the following: + + - upper: All words are upper-case. + - lower: All words are lower-case. + - pascal: All words are title-case or upper-case. Note that the variable may still have separators. + - camel: First word is lower-case, the rest are title-case or upper-case. Variable may still have separators. + - mixed: Any other mixing of word casing. Never occurs if there are no separators. + - unknown: Variable contains no words. + +Also returns the first separator character, or False if there isn't one. +""" + + +class CaseConverter(object): + def __init__(self, detect_acronyms=True, acronyms=None, toggle_cases=None): + self.detect_acronyms = detect_acronyms + if acronyms: + self.acronyms = self._clean_acronyms(acronyms) + + def _clean_acronyms(self, acronyms): + """ Use advanced acronym detection with list + Sanitize self.acronyms list by discarding invalid self.acronyms and + normalizing valid ones to upper-case. """ + validacronym = re.compile('^[a-zA-Z0-9]+$') + unsafeacronyms = acronyms + acronyms = [] + for a in unsafeacronyms: + if validacronym.match(a): + # Sanitizing acronyms to include only letters. + a = re.sub('[^a-zA-Z]', '', a) + acronyms.append(a.upper()) + else: + print( + "CaseConverter: acronym '{}' was discarded for being invalid".format(a)) + return acronyms + + def _check_acronym(self, words, s, i, acronyms=None): + def simple_acronym_detection(words, s, i): + # Combine each letter into a single string. + acronym = ''.join(words[s:i]) + + # Remove original letters in word list. + for j in xrange(s, i): + del words[s] + + # Replace them with new word grouping. + words.insert(s, ''.join(acronym)) + + return s + + def advanced_acronym_detection(words, s, i, acronyms): + # Combine each letter into single string. + acstr = ''.join(words[s:i]) + + # List of ranges representing found self.acronyms. + rangeList = [] + # Set of remaining letters. + notRange = set(range(len(acstr))) + + # Search for each acronym in acstr. + for acronym in acronyms: + + rac = re.compile(acronym) + + # Loop to find all instances of the acronym + n = 0 + while True: + m = rac.search(acstr, n) + if not m: + break + + a, b = m.start(), m.end() + n = b + + # Make sure found acronym doesn't overlap with + # others. + ok = True + for r in rangeList: + if a < r[1] and b > r[0]: + ok = False + break + + if ok: + rangeList.append((a, b)) + for j in xrange(a, b): + notRange.remove(j) + + # Add remaining letters as ranges. + for nr in notRange: + rangeList.append((nr, nr + 1)) + + """ No ranges will overlap, so it's safe to sort by lower bound, + which sort() will do by default.""" + rangeList.sort() + + # Remove original letters in word list. + for j in xrange(s, i): + del words[s] + + # Replace them with new word grouping. + for j in xrange(len(rangeList)): + r = rangeList[j] + words.insert(s + j, acstr[r[0]:r[1]]) + + return s + len(rangeList) - 1 + + # decide for method of acronym detection + if not acronyms: + return simple_acronym_detection(words, s, i) + """ Check a run of words represented by the range [s, i]. Should + return last index of new word groups.""" + + # Letter-run detector + # Index of current word. + else: + return advanced_acronym_detection(words, s, i, acronyms) + + def _parse_text(self, text, *, preserve_case=False): + # TODO: include unicode characters. + lower = re.compile('^[a-z0-9]$') # TODO check for lower downstream + upper = re.compile('^[A-Z]$') + sep = re.compile('^[^a-zA-Z0-9]$') + notsep = re.compile('^[a-zA-Z0-9]$') + + words = [] + hasSep = False + + """ Index of current character. Initially 1 because we don't want + to check if the 0th character is a boundary. """ + i = 1 + # Index of first character in a sequence + s = 0 + # Previous character. + p = text[0:1] + + # Treat an all-caps variable as lower-case, so that every letter isn't + # counted as a boundary. + wasUpper = False + if text.isupper(): + text = text.lower() + wasUpper = True + + # Iterate over each character, checking for boundaries, or places where + # the variable should divided. We need a while loop here for skipping + while i <= len(text): + + c = text[i:i + 1] + + split = False + if i < len(text): + # Detect upper-case letter as boundary. + if upper.match(c): + split = True + # Detect transition from separator to not separator. + elif notsep.match(c) and sep.match(p): + split = True + # Detect transition not separator to separator. + elif sep.match(c) and notsep.match(p): + split = True + else: + # The loop goes one extra iteration so that it can handle the + # remaining text after the last boundary. + split = True + + if split: + if notsep.match(p): + words.append(text[s:i]) + else: + # Variable contains at least one separator. + # Use the first one as the variable's primary separator. + if not hasSep: + hasSep = text[s:s + 1] + + # Use None to indicate a separator in the word list. + words.append(None) + """ If separators weren't included in the list, then breaks + between upper-case sequences ("AAA_BBB") would be + disregarded; the letter-run detector would count them as one sequence ("AAABBB").""" + s = i + + i = i + 1 + p = c + + if self.detect_acronyms: + i = 0 + # Index of first letter in run. + s = None + + # Find runs of single upper-case letters. + while i < len(words): + word = words[i] + if word and upper.match(word): + if not s: + s = i + elif s: + i = self._check_acronym(words, s, i, self.acronyms) + 1 + s = None + + i += 1 + + if s: # TODO: does this make sense? return value is not caught + self._check_acronym(words, s, i, self.acronyms) + + """Separators are no longer needed, so they can be removed. They + *should* # be removed, since it's supposed to be a *word* list.""" + words = [w for w in words if w] + + # Determine case type. + caseType = 'unknown' + if wasUpper: + caseType = 'upper' + elif text.islower(): + caseType = 'lower' + elif words: + camelCase = words[0].islower() + pascalCase = words[0].istitle() or words[0].isupper() + + if camelCase or pascalCase: + for word in words[1:]: + c = word.istitle() or word.isupper() + camelCase &= c + pascalCase &= c + if not c: + break + + if camelCase: + caseType = 'camel' + elif pascalCase: + caseType = 'pascal' + else: + caseType = 'mixed' + + if preserve_case: + if wasUpper: + words = [w.upper() for w in words] + else: + """Normalize case of each word to PascalCase. From there, other + cases can be worked out easily.""" + for i in xrange(len(words)): + if self.detect_acronyms: + if self.acronyms: + if words[i].upper() in self.acronyms: + # Convert known self.acronyms to upper-case. + words[i] = words[i].upper() + else: + # Capitalize everything else. + words[i] = words[i].capitalize() + else: + # Fallback behavior: Preserve case on upper-case words. + if not words[i].isupper(): + words[i] = words[i].capitalize() + else: + words[i] = words[i].capitalize() + + return words, caseType, hasSep + + def _determine_case(self, string): + junk, case, sep = self._parse_text(string, preserve_case=True) + if case == 'pascal' and not sep: + return 'pascal' + elif case == 'lower' and sep == '_': + return 'snake' + elif case == 'camel' and not sep: + return 'camel' + else: + return 'unknown' + + def to_lower_case(self, text): + words, *junk = self._parse_text(text) + return ' '.join([w.lower() for w in words]) + + def to_upper_case(self, text): + words, *junk = self._parse_text(text) + return ' '.join([w.upper() for w in words]) + + def to_capital_case(self, text): + words, *junk = self._parse_text(text) + string = ''.join([w.lower() for w in words]) + return string.capitalize() + + def to_title_case(self, text): + words, *junk = self._parse_text(text) + return ' '.join([w.lower().capitalize() for w in words]) + + def to_snake_case(self, text): + words, *junk = self._parse_text(text) + return '_'.join([w.lower() for w in words]) + + def to_screaming_snake_case(self, text): + """Also called CONST_CASE""" + words, *junk = self._parse_text(text) + return '_'.join([w.upper() for w in words]) + + def to_pascal_case(self, text): + words, *junk = self._parse_text(text) + return ''.join(words) + + def to_camel_case(self, text): + words, *junk = self._parse_text(text) + words[0] = words[0].lower() + return ''.join(words) + + def to_dot_case(self, text): + words, *junk = self._parse_text(text) + return '.'.join([w.lower() for w in words]) + + def to_dash_case(self, text): + """Also called spinal-case""" + words, *junk = self._parse_text(text) + return '-'.join([w.lower() for w in words]) + + def to_slash_case(self, text): + """Also called path/case""" + words, *junk = self._parse_text(text, preserve_case=True) + return '/'.join(words) + + def to_backslash_case(self, text): + words, *junk = self._parse_text(text, preserve_case=True) + return '\\'.join(words) + + def to_separate_words(self, text): + words, *junk = self._parse_text(text, preserve_case=True) + return ' '.join(words) + + def toggle_case(self, text): + + func_dict = { + 'lower': self.to_lower_case, + 'upper': self.to_upper_case, + 'capital': self.to_capital_case, + 'snake': self.to_snake_case, + 'screaming_snake': self.to_screaming_snake_case, + 'camel': self.to_camel_case, + 'pascal': self.to_pascal_case, + 'dot': self.to_dot_case, + 'dash': self.to_dash_case, + 'slash': self.to_slash_case, + 'backslash': self.to_backslash_case, + 'title': self.to_title_case, + 'separate_words': self.to_separate_words + } + + case = self._determine_case(text) + + try: + target_case = self.toggle_cases[case] + except KeyError: + if not self.toggle_cases: + print("CaseConverter: Toggle dictionary empty.") + print("CaseConverter: Toggling '{}' not supported.".format(case)) + else: + return func_dict[target_case](text) diff --git a/case_parse.py b/case_parse.py index b7f17a8..b509f48 100644 --- a/case_parse.py +++ b/case_parse.py @@ -2,7 +2,8 @@ import sys PYTHON = sys.version_info[0] -if 3 == PYTHON: xrange = range +if 3 == PYTHON: + xrange = range """ @@ -19,224 +20,342 @@ Also returns the first separator character, or False if there isn't one. """ -def parseVariable(var, detectAcronyms=True, acronyms=[], preserveCase=False): - # TODO: include unicode characters. - lower = re.compile('^[a-z0-9]$') - upper = re.compile('^[A-Z]$') - sep = re.compile('^[^a-zA-Z0-9]$') - notsep = re.compile('^[a-zA-Z0-9]$') - - words = [] - hasSep = False - - # Index of current character. Initially 1 because we don't want to check - # if the 0th character is a boundary. - i = 1 - # Index of first character in a sequence - s = 0 - # Previous character. - p = var[0:1] - - # Treat an all-caps variable as lower-case, so that every letter isn't - # counted as a boundary. - wasUpper = False - if var.isupper(): - var = var.lower() - wasUpper = True - - # Iterate over each character, checking for boundaries, or places where - # the variable should divided. - while i <= len(var): - c = var[i:i+1] - - split = False - if i < len(var): - # Detect upper-case letter as boundary. - if upper.match(c): - split = True - # Detect transition from separator to not separator. - elif notsep.match(c) and sep.match(p): - split = True - # Detect transition not separator to separator. - elif sep.match(c) and notsep.match(p): - split = True - else: - # The loop goes one extra iteration so that it can handle the - # remaining text after the last boundary. - split = True - - if split: - if notsep.match(p): - words.append(var[s:i]) - else: - # Variable contains at least one separator. - # Use the first one as the variable's primary separator. - if not hasSep: hasSep = var[s:s+1] - - # Use None to indicate a separator in the word list. - words.append(None) - # If separators weren't included in the list, then breaks - # between upper-case sequences ("AAA_BBB") would be - # disregarded; the letter-run detector would count them as one - # sequence ("AAABBB"). - s = i - - i = i + 1 - p = c - - if detectAcronyms: - if acronyms: - # Use advanced acronym detection with list - - # Sanitize acronyms list by discarding invalid acronyms and - # normalizing valid ones to upper-case. - validacronym = re.compile('^[a-zA-Z0-9]+$') - unsafeacronyms = acronyms - acronyms = [] - for a in unsafeacronyms: - if validacronym.match(a): - acronyms.append(a.upper()) - else: - print("Case Conversion: acronym '%s' was discarded for being invalid" % a) - - # Check a run of words represented by the range [s, i]. Should - # return last index of new word groups. - def checkAcronym(s, i): - # Combine each letter into single string. - acstr = ''.join(words[s:i]) - - # List of ranges representing found acronyms. - rangeList = [] - # Set of remaining letters. - notRange = set(range(len(acstr))) - - # Search for each acronym in acstr. - for acronym in acronyms: - #TODO: Sanitize acronyms to include only letters. - rac = re.compile(acronym) - - # Loop so that all instances of the acronym are found, instead - # of just the first. - n = 0 - while True: - m = rac.search(acstr, n) - if not m: break - - a, b = m.start(), m.end() - n = b - - # Make sure found acronym doesn't overlap with others. - ok = True - for r in rangeList: - if a < r[1] and b > r[0]: - ok = False - break - - if ok: - rangeList.append((a, b)) - for j in xrange(a, b): - notRange.remove(j) - - # Add remaining letters as ranges. - for nr in notRange: - rangeList.append((nr, nr+1)) - - # No ranges will overlap, so it's safe to sort by lower bound, - # which sort() will do by default. - rangeList.sort() - - # Remove original letters in word list. - for j in xrange(s, i): del words[s] - - # Replace them with new word grouping. - for j in xrange(len(rangeList)): - r = rangeList[j] - words.insert(s+j, acstr[r[0]:r[1]]) - - return s+len(rangeList)-1 - else: - # Fallback to simple acronym detection. - def checkAcronym(s, i): - # Combine each letter into a single string. - acronym = ''.join(words[s:i]) - # Remove original letters in word list. - for j in xrange(s, i): del words[s] - # Replace them with new word grouping. - words.insert(s,''.join(acronym)) - - return s +class CaseConverter(object): + def __init__(self, detect_acronyms=True, acronyms=None, toggle_cases=None): + self.detect_acronyms = detect_acronyms + if acronyms: + self.acronyms = self._clean_acronyms(acronyms) + + def _clean_acronyms(self, acronyms): + """ Use advanced acronym detection with list + Sanitize self.acronyms list by discarding invalid self.acronyms and + normalizing valid ones to upper-case. """ + validacronym = re.compile('^[a-zA-Z0-9]+$') + unsafeacronyms = acronyms + acronyms = [] + for a in unsafeacronyms: + if validacronym.match(a): + # Sanitizing acronyms to include only letters. + a = re.sub('[^a-zA-Z]', '', a) + acronyms.append(a.upper()) + else: + print( + "CaseConverter: acronym '{}' was discarded for being invalid".format(a)) + return acronyms + + def _check_acronym(self, words, s, i, acronyms=None): + def simple_acronym_detection(words, s, i): + # Combine each letter into a single string. + acronym = ''.join(words[s:i]) + + # Remove original letters in word list. + for j in xrange(s, i): + del words[s] + + # Replace them with new word grouping. + words.insert(s, ''.join(acronym)) + + return s + + def advanced_acronym_detection(words, s, i, acronyms): + # Combine each letter into single string. + acstr = ''.join(words[s:i]) + + # List of ranges representing found self.acronyms. + rangeList = [] + # Set of remaining letters. + notRange = set(range(len(acstr))) + + # Search for each acronym in acstr. + for acronym in acronyms: + + rac = re.compile(acronym) + + # Loop to find all instances of the acronym + n = 0 + while True: + m = rac.search(acstr, n) + if not m: + break + + a, b = m.start(), m.end() + n = b + + # Make sure found acronym doesn't overlap with + # others. + ok = True + for r in rangeList: + if a < r[1] and b > r[0]: + ok = False + break + + if ok: + rangeList.append((a, b)) + for j in xrange(a, b): + notRange.remove(j) + + # Add remaining letters as ranges. + for nr in notRange: + rangeList.append((nr, nr + 1)) + + """ No ranges will overlap, so it's safe to sort by lower bound, + which sort() will do by default.""" + rangeList.sort() + + # Remove original letters in word list. + for j in xrange(s, i): + del words[s] + + # Replace them with new word grouping. + for j in xrange(len(rangeList)): + r = rangeList[j] + words.insert(s + j, acstr[r[0]:r[1]]) + + return s + len(rangeList) - 1 + + # decide for method of acronym detection + if not acronyms: + return simple_acronym_detection(words, s, i) + """ Check a run of words represented by the range [s, i]. Should + return last index of new word groups.""" # Letter-run detector - # Index of current word. - i = 0 - # Index of first letter in run. - s = None - - # Find runs of single upper-case letters. - while i < len(words): - word = words[i] - if word != None and upper.match(word): - if s == None: s = i - elif s != None: - i = checkAcronym(s, i) + 1 - s = None - - i += 1 - - if s != None: - checkAcronym(s, i) - - # Separators are no longer needed, so they can be removed. They *should* - # be removed, since it's supposed to be a *word* list. - words = [w for w in words if w != None] - - # Determine case type. - caseType = 'unknown' - if wasUpper: - caseType = 'upper' - elif var.islower(): - caseType = 'lower' - elif len(words) > 0: - camelCase = words[0].islower() - pascalCase = words[0].istitle() or words[0].isupper() - - if camelCase or pascalCase: - for word in words[1:]: - c = word.istitle() or word.isupper() - camelCase &= c - pascalCase &= c - if not c: break - - if camelCase: - caseType = 'camel' - elif pascalCase: - caseType = 'pascal' else: - caseType = 'mixed' + return advanced_acronym_detection(words, s, i, acronyms) + + def _parse_text(self, text, *, preserve_case=False): + # TODO: include unicode characters. + lower = re.compile('^[a-z0-9]$') # TODO check for lower downstream + upper = re.compile('^[A-Z]$') + sep = re.compile('^[^a-zA-Z0-9]$') + notsep = re.compile('^[a-zA-Z0-9]$') + + words = [] + hasSep = False + + """ Index of current character. Initially 1 because we don't want + to check if the 0th character is a boundary. """ + i = 1 + # Index of first character in a sequence + s = 0 + # Previous character. + p = text[0:1] + + # Treat an all-caps variable as lower-case, so that every letter isn't + # counted as a boundary. + wasUpper = False + if text.isupper(): + text = text.lower() + wasUpper = True + + # Iterate over each character, checking for boundaries, or places where + # the variable should divided. We need a while loop here for skipping + while i <= len(text): + + c = text[i:i + 1] + + split = False + if i < len(text): + # Detect upper-case letter as boundary. + if upper.match(c): + split = True + # Detect transition from separator to not separator. + elif notsep.match(c) and sep.match(p): + split = True + # Detect transition not separator to separator. + elif sep.match(c) and notsep.match(p): + split = True + else: + # The loop goes one extra iteration so that it can handle the + # remaining text after the last boundary. + split = True - if preserveCase: - if wasUpper: - words = [w.upper() for w in words] - else: - # Normalize case of each word to PascalCase. From there, other cases - # can be worked out easily. - for i in xrange(len(words)): - if detectAcronyms: - if acronyms: - if words[i].upper() in acronyms: - # Convert known acronyms to upper-case. - words[i] = words[i].upper() - else: - # Capitalize everything else. - words[i] = words[i].capitalize() + if split: + if notsep.match(p): + words.append(text[s:i]) else: - # Fallback behavior: Preserve case on upper-case words. - if not words[i].isupper(): - words[i] = words[i].capitalize() + # Variable contains at least one separator. + # Use the first one as the variable's primary separator. + if not hasSep: + hasSep = text[s:s + 1] + + # Use None to indicate a separator in the word list. + words.append(None) + """ If separators weren't included in the list, then breaks + between upper-case sequences ("AAA_BBB") would be + disregarded; the letter-run detector would count them as one sequence ("AAABBB").""" + s = i + + i = i + 1 + p = c + + if self.detect_acronyms: + i = 0 + # Index of first letter in run. + s = None + + # Find runs of single upper-case letters. + while i < len(words): + word = words[i] + if word and upper.match(word): + if not s: + s = i + elif s: + i = self._check_acronym(words, s, i, self.acronyms) + 1 + s = None + + i += 1 + + if s: # TODO: does this make sense? return value is not caught + self._check_acronym(words, s, i, self.acronyms) + + """Separators are no longer needed, so they can be removed. They + *should* # be removed, since it's supposed to be a *word* list.""" + words = [w for w in words if w] + + # Determine case type. + caseType = 'unknown' + if wasUpper: + caseType = 'upper' + elif text.islower(): + caseType = 'lower' + elif words: + camelCase = words[0].islower() + pascalCase = words[0].istitle() or words[0].isupper() + + if camelCase or pascalCase: + for word in words[1:]: + c = word.istitle() or word.isupper() + camelCase &= c + pascalCase &= c + if not c: + break + + if camelCase: + caseType = 'camel' + elif pascalCase: + caseType = 'pascal' else: - words[i] = words[i].capitalize() + caseType = 'mixed' - return words, caseType, hasSep + if preserve_case: + if wasUpper: + words = [w.upper() for w in words] + else: + """Normalize case of each word to PascalCase. From there, other + cases can be worked out easily.""" + for i in xrange(len(words)): + if self.detect_acronyms: + if self.acronyms: + if words[i].upper() in self.acronyms: + # Convert known self.acronyms to upper-case. + words[i] = words[i].upper() + else: + # Capitalize everything else. + words[i] = words[i].capitalize() + else: + # Fallback behavior: Preserve case on upper-case words. + if not words[i].isupper(): + words[i] = words[i].capitalize() + else: + words[i] = words[i].capitalize() + + return words, caseType, hasSep + + def _determine_case(self, string): + junk, case, sep = self._parse_text(string, preserve_case=True) + if case == 'pascal' and not sep: + return 'pascal' + elif case == 'lower' and sep == '_': + return 'snake' + elif case == 'camel' and not sep: + return 'camel' + else: + return 'unknown' + + def to_lower_case(self, text): + words, *junk = self._parse_text(text) + return ' '.join([w.lower() for w in words]) + + def to_upper_case(self, text): + words, *junk = self._parse_text(text) + return ' '.join([w.upper() for w in words]) + + def to_capital_case(self, text): + words, *junk = self._parse_text(text) + string = ''.join([w.lower() for w in words]) + return string.capitalize() + + def to_title_case(self, text): + words, *junk = self._parse_text(text) + return ' '.join([w.lower().capitalize() for w in words]) + + def to_snake_case(self, text): + words, *junk = self._parse_text(text) + return '_'.join([w.lower() for w in words]) + + def to_screaming_snake_case(self, text): + """Also called CONST_CASE""" + words, *junk = self._parse_text(text) + return '_'.join([w.upper() for w in words]) + + def to_pascal_case(self, text): + words, *junk = self._parse_text(text) + return ''.join(words) + + def to_camel_case(self, text): + words, *junk = self._parse_text(text) + words[0] = words[0].lower() + return ''.join(words) + + def to_dot_case(self, text): + words, *junk = self._parse_text(text) + return '.'.join([w.lower() for w in words]) + + def to_dash_case(self, text): + """Also called spinal-case""" + words, *junk = self._parse_text(text) + return '-'.join([w.lower() for w in words]) + + def to_slash_case(self, text): + """Also called path/case""" + words, *junk = self._parse_text(text, preserve_case=True) + return '/'.join(words) + + def to_backslash_case(self, text): + words, *junk = self._parse_text(text, preserve_case=True) + return '\\'.join(words) + + def to_separate_words(self, text): + words, *junk = self._parse_text(text, preserve_case=True) + return ' '.join(words) + + def toggle_case(self, text): + + func_dict = { + 'lower': self.to_lower_case, + 'upper': self.to_upper_case, + 'capital': self.to_capital_case, + 'snake': self.to_snake_case, + 'screaming_snake': self.to_screaming_snake_case, + 'camel': self.to_camel_case, + 'pascal': self.to_pascal_case, + 'dot': self.to_dot_case, + 'dash': self.to_dash_case, + 'slash': self.to_slash_case, + 'backslash': self.to_backslash_case, + 'title': self.to_title_case, + 'separate_words': self.to_separate_words + } + + case = self._determine_case(text) + + try: + target_case = self.toggle_cases[case] + except KeyError: + print("CaseConverter: Toggling '{}' not supported.".format(case)) + else: + return func_dict[target_case](text)