From 4232932f584c2148b5ffba5608d5302941d6c51a Mon Sep 17 00:00:00 2001 From: bfelder Date: Thu, 3 Aug 2017 23:27:54 +0200 Subject: [PATCH 1/8] Initial push - quick panel working --- case_conversion.py | 144 +++++++++++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 58 deletions(-) diff --git a/case_conversion.py b/case_conversion.py index aa2f011..d0636b5 100644 --- a/case_conversion.py +++ b/case_conversion.py @@ -3,6 +3,7 @@ import re import sys +# TODO: remove this as we will be targeting only ST3 PYTHON = sys.version_info[0] if 3 == PYTHON: @@ -12,8 +13,39 @@ # Python 2 and ST2 import case_parse - +# TODO sort by most used +# TODO is this how setting files are defined? SETTINGS_FILE = "CaseConversion.sublime-settings" +PLUGIN_NAME = 'CaseConversion' + +""" case conversion methods """ + + +class Vars: + original = "" + + +def to_original(text, detectAcronyms, acronyms): + """ to original """ + # words, case, sep = case_parse.parseVariable( + # Vars.original, detectAcronyms, acronyms) + # return ''.join([w for w in words]) + return Vars.original + + +def to_lower_case(text, detectAcronyms, acronyms): + words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) + return ''.join([w.lower() for w in words]) + + +def to_upper_case(text, detectAcronyms, acronyms): + words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) + return ''.join([w.upper() for w in words]) + + +def to_title_case(text, detectAcronyms, acronyms): + words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) + return ''.join([w.lower().title() for w in words]) def to_snake_case(text, detectAcronyms, acronyms): @@ -48,29 +80,39 @@ def to_dash_case(text, detectAcronyms, acronyms): def to_slash(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms, True) + 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) + 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) + words, case, sep = case_parse.parseVariable( + text, detectAcronyms, acronyms, True) return ' '.join(words) +# elif case == pascal lower camel -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 + +LABEL_KEY_FUNC = ( + (["lowercase", "0"], "", to_lower_case), + (["UPPERCASE", "1"], "", to_upper_case), + (["Titlecase", ""], "", to_title_case), + (["snake_case", ""], "", to_snake_case), + (["SCREAMING_SNAKE_CASE", ""], "", to_screaming_snake_case), + (["PascalCase", ""], "", to_pascal_case), + (["camelCase", ""], "", to_camel_case), + (["dot.case", ""], "", to_dot_case), + (["dash-case", ""], "", to_dash_case), + (["slash\case", ""], "", to_slash), + (["backslash/case", ""], "", to_backslash), + (["separate words", ""], "", to_separate_words) +) def run_on_selections(view, edit, func): @@ -85,59 +127,45 @@ def run_on_selections(view, edit, func): for s in view.sel(): region = s if s else view.word(s) - text = view.substr(region) + if not Vars.original: # save original in the beginning + Vars.original = view.substr(region) + + text = Vars.original + # Preserve leading and trailing whitespace - leading = text[:len(text)-len(text.lstrip())] + leading = text[:len(text) - len(text.lstrip())] trailing = text[len(text.rstrip()):] - new_text = leading + func(text.strip(), detectAcronyms, acronyms) + trailing + new_text = leading + \ + func(text.strip(), detectAcronyms, acronyms) + trailing if new_text != text: - view.replace(edit, region, new_text) + view.run_command( + 'convert', {"region_a": region.a, "region_b": region.b, + "new_text": 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 ConvertCommand(sublime_plugin.TextCommand): + def run(self, edit, region_a, region_b, new_text): + region = sublime.Region(region_a, region_b) + self.view.replace(edit, region, new_text) -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) +def panel_mgr(view, edit): + def callback(opt): + index_func_dict = {i: v[2] for i, v in enumerate(LABEL_KEY_FUNC)} + try: + convert_to = index_func_dict[opt] + except KeyError: + print("KeyError - opt: ", opt) + convert_to = to_original + finally: + run_on_selections(view, edit, convert_to) -class ConvertToSeparateWords(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_separate_words) + labels = [i[0] for i in LABEL_KEY_FUNC] + sublime.active_window().show_quick_panel( + labels, callback, sublime.MONOSPACE_FONT, 0, callback) -class ConvertToSlash(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, to_slash ) - -class ConvertToBackSlash(sublime_plugin.TextCommand): +class ToggleCaseConverterCommand(sublime_plugin.TextCommand): def run(self, edit): - run_on_selections(self.view, edit, to_backslash ) + panel_mgr(self.view, edit) From c2d1ee8eb4af631b57c6720b3cda73d4c58e9d3b Mon Sep 17 00:00:00 2001 From: bfelder Date: Sun, 6 Aug 2017 20:16:50 +0200 Subject: [PATCH 2/8] Update case_conversion.py --- case_conversion.py | 188 +++++++++++++++------------------------------ 1 file changed, 63 insertions(+), 125 deletions(-) diff --git a/case_conversion.py b/case_conversion.py index d0636b5..d298aaf 100644 --- a/case_conversion.py +++ b/case_conversion.py @@ -3,169 +3,107 @@ import re import sys -# TODO: remove this as we will be targeting only ST3 PYTHON = sys.version_info[0] if 3 == PYTHON: # Python 3 and ST3 - from . import case_parse + from .case_parse import CaseConverter else: # Python 2 and ST2 - import case_parse - -# TODO sort by most used -# TODO is this how setting files are defined? -SETTINGS_FILE = "CaseConversion.sublime-settings" -PLUGIN_NAME = 'CaseConversion' - -""" case conversion methods """ - - -class Vars: - original = "" - - -def to_original(text, detectAcronyms, acronyms): - """ to original """ - # words, case, sep = case_parse.parseVariable( - # Vars.original, detectAcronyms, acronyms) - # return ''.join([w for w in words]) - return Vars.original - - -def to_lower_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return ''.join([w.lower() for w in words]) - - -def to_upper_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return ''.join([w.upper() for w in words]) + from case_parse import CaseConverter -def to_title_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return ''.join([w.lower().title() for w in words]) - - -def to_snake_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return '_'.join([w.lower() for w in words]) - +SETTINGS_FILE = "CaseConversion.sublime-settings" +settings = sublime.load_settings(SETTINGS_FILE) +detect_acronyms = settings.get("detect_acronyms", True) +use_list = settings.get("use_acronyms_list", True) +if use_list: + acronyms = settings.get("acronyms", None) +else: + acronyms = None -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]) +converter = CaseConverter(detect_acronyms, acronyms) -def to_pascal_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return ''.join(words) +def run_on_selections(view, edit, convert_func): + 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()):] + converted_text = convert_func(text.strip()) + new_text = leading + converted_text + trailing + if new_text != text: + view.replace(edit, region, new_text) -def to_camel_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - words[0] = words[0].lower() - return ''.join(words) +class ToggleCasesCommand(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.toggle_case) -def to_dot_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return '.'.join([w.lower() for w in words]) +class ConvertToLowerCommand(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_lower_case) -def to_dash_case(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable(text, detectAcronyms, acronyms) - return '-'.join([w.lower() for w in words]) +class ConvertToUpperCommand(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_upper_case) -def to_slash(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable( - text, detectAcronyms, acronyms, True) - return '/'.join(words) +class ConvertToCapitalCommand(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_capital_case) -def to_backslash(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable( - text, detectAcronyms, acronyms, True) - return '\\'.join(words) +class ConvertToSnakeCommand(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_snake_case) -def to_separate_words(text, detectAcronyms, acronyms): - words, case, sep = case_parse.parseVariable( - text, detectAcronyms, acronyms, True) - return ' '.join(words) -# elif case == pascal lower camel +class ConvertToScreamingSnakeCommand(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_screaming_snake_case) -LABEL_KEY_FUNC = ( - (["lowercase", "0"], "", to_lower_case), - (["UPPERCASE", "1"], "", to_upper_case), - (["Titlecase", ""], "", to_title_case), - (["snake_case", ""], "", to_snake_case), - (["SCREAMING_SNAKE_CASE", ""], "", to_screaming_snake_case), - (["PascalCase", ""], "", to_pascal_case), - (["camelCase", ""], "", to_camel_case), - (["dot.case", ""], "", to_dot_case), - (["dash-case", ""], "", to_dash_case), - (["slash\case", ""], "", to_slash), - (["backslash/case", ""], "", to_backslash), - (["separate words", ""], "", to_separate_words) -) +class ConvertToCamel(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_camel_case) -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 +class ConvertToPascal(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_pascal_case) - for s in view.sel(): - region = s if s else view.word(s) - if not Vars.original: # save original in the beginning - Vars.original = view.substr(region) +class ConvertToDot(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_dot_case) - text = Vars.original - # 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.run_command( - 'convert', {"region_a": region.a, "region_b": region.b, - "new_text": new_text}) +class ConvertToDash(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_dash_case) -class ConvertCommand(sublime_plugin.TextCommand): - def run(self, edit, region_a, region_b, new_text): - region = sublime.Region(region_a, region_b) - self.view.replace(edit, region, new_text) +class ConvertToSlash(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_slash_case) -def panel_mgr(view, edit): - def callback(opt): - index_func_dict = {i: v[2] for i, v in enumerate(LABEL_KEY_FUNC)} +class ConvertToBackSlash(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_backslash_case) - try: - convert_to = index_func_dict[opt] - except KeyError: - print("KeyError - opt: ", opt) - convert_to = to_original - finally: - run_on_selections(view, edit, convert_to) - labels = [i[0] for i in LABEL_KEY_FUNC] - sublime.active_window().show_quick_panel( - labels, callback, sublime.MONOSPACE_FONT, 0, callback) +class ConvertToTitleCommand(sublime_plugin.TextCommand): + def run(self, edit): + run_on_selections(self.view, edit, converter.to_title_case) -class ToggleCaseConverterCommand(sublime_plugin.TextCommand): +class ConvertToSeparateWords(sublime_plugin.TextCommand): def run(self, edit): - panel_mgr(self.view, edit) + run_on_selections(self.view, edit, converter.to_separate_words) From 4a4a49b565dfcf9389df6e087d15af80b9504585 Mon Sep 17 00:00:00 2001 From: bfelder Date: Sun, 6 Aug 2017 20:17:17 +0200 Subject: [PATCH 3/8] Update case_parse.py --- case_parse.py | 548 +++++++++++++++++++++++++++++++------------------- 1 file changed, 336 insertions(+), 212 deletions(-) diff --git a/case_parse.py b/case_parse.py index b7f17a8..e27123b 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,347 @@ 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={'pascal': 'snake', + 'lower': 'camel', + 'camel': 'pascal'}): + 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 get_case(self, string): + pass + + # def _convert_text(self) + + 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): + junk, case, sep = self._parse_text(text, preserve_case=True) + + 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 + } + + try: + target_case = self.toggle_cases[case] + except KeyError: + raise e + else: + return func_dict[target_case](text) + + if case == 'pascal' and not sep: + return self.to_snake_case(text) + elif case == 'lower' and sep == '_': + return self.to_camel_case(text) + elif case == 'camel' and not sep: + return self.to_pascal_case(text) + else: + return text From 7b52f5e0944721d1682b13cb1ff1d3ad47911461 Mon Sep 17 00:00:00 2001 From: bfelder Date: Sun, 6 Aug 2017 20:30:38 +0200 Subject: [PATCH 4/8] Update case_parse.py --- case_parse.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/case_parse.py b/case_parse.py index e27123b..d687228 100644 --- a/case_parse.py +++ b/case_parse.py @@ -268,10 +268,16 @@ def _parse_text(self, text, *, preserve_case=False): return words, caseType, hasSep - def get_case(self, string): - pass - - # def _convert_text(self) + 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) @@ -331,7 +337,6 @@ def to_separate_words(self, text): return ' '.join(words) def toggle_case(self, text): - junk, case, sep = self._parse_text(text, preserve_case=True) func_dict = { 'lower': self.to_lower_case, @@ -349,18 +354,11 @@ def toggle_case(self, text): 'separate_words': self.to_separate_words } + case = self._determine_case(text) + try: - target_case = self.toggle_cases[case] + target_case = self.toggle_cases[case] except KeyError: - raise e + print("CaseConverter: Toggling '{}' not supported.".format(case)) else: return func_dict[target_case](text) - - if case == 'pascal' and not sep: - return self.to_snake_case(text) - elif case == 'lower' and sep == '_': - return self.to_camel_case(text) - elif case == 'camel' and not sep: - return self.to_pascal_case(text) - else: - return text From a5b68bdce18339639f21f45756ec6124df554c37 Mon Sep 17 00:00:00 2001 From: bfelder Date: Sun, 6 Aug 2017 21:43:22 +0200 Subject: [PATCH 5/8] Update case_conversion.py --- case_conversion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/case_conversion.py b/case_conversion.py index d298aaf..84db7cc 100644 --- a/case_conversion.py +++ b/case_conversion.py @@ -22,10 +22,12 @@ else: acronyms = None +toggle_cases = settings.get("toggle_cases", None) + converter = CaseConverter(detect_acronyms, acronyms) -def run_on_selections(view, edit, convert_func): +def run_on_selections(view, edit, convert_func, toggle_cases): for s in view.sel(): region = s if s else view.word(s) From 08a62d7e752b497c79599aa5e6ca5f79ce993d8e Mon Sep 17 00:00:00 2001 From: bfelder Date: Sun, 6 Aug 2017 21:50:43 +0200 Subject: [PATCH 6/8] Update CaseConversion.sublime-settings --- CaseConversion.sublime-settings | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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" + } } From b23c2bc5fc860ce9116e437ce16fd1fb8c8fe356 Mon Sep 17 00:00:00 2001 From: bfelder Date: Sun, 6 Aug 2017 21:55:59 +0200 Subject: [PATCH 7/8] Update case_conversion.py --- case_conversion.py | 464 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 358 insertions(+), 106 deletions(-) diff --git a/case_conversion.py b/case_conversion.py index 84db7cc..a29a9ce 100644 --- a/case_conversion.py +++ b/case_conversion.py @@ -1,111 +1,363 @@ -import sublime -import sublime_plugin import re import sys PYTHON = sys.version_info[0] - if 3 == PYTHON: - # Python 3 and ST3 - from .case_parse import CaseConverter -else: - # Python 2 and ST2 - from case_parse import CaseConverter - - -SETTINGS_FILE = "CaseConversion.sublime-settings" -settings = sublime.load_settings(SETTINGS_FILE) -detect_acronyms = settings.get("detect_acronyms", True) -use_list = settings.get("use_acronyms_list", True) -if use_list: - acronyms = settings.get("acronyms", None) -else: - acronyms = None - -toggle_cases = settings.get("toggle_cases", None) - -converter = CaseConverter(detect_acronyms, acronyms) - - -def run_on_selections(view, edit, convert_func, toggle_cases): - 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()):] - converted_text = convert_func(text.strip()) - new_text = leading + converted_text + trailing - if new_text != text: - view.replace(edit, region, new_text) - - -class ToggleCasesCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.toggle_case) - - -class ConvertToLowerCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_lower_case) - - -class ConvertToUpperCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_upper_case) - - -class ConvertToCapitalCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_capital_case) - - -class ConvertToSnakeCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_snake_case) - - -class ConvertToScreamingSnakeCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_screaming_snake_case) - - -class ConvertToCamel(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_camel_case) - - -class ConvertToPascal(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_pascal_case) - - -class ConvertToDot(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_dot_case) - - -class ConvertToDash(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_dash_case) - - -class ConvertToSlash(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_slash_case) - - -class ConvertToBackSlash(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_backslash_case) - - -class ConvertToTitleCommand(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_title_case) - - -class ConvertToSeparateWords(sublime_plugin.TextCommand): - def run(self, edit): - run_on_selections(self.view, edit, converter.to_separate_words) + 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) From 3ec71574eecf63e9e80c39e643d6f338e374f2cf Mon Sep 17 00:00:00 2001 From: bfelder Date: Sun, 6 Aug 2017 21:59:18 +0200 Subject: [PATCH 8/8] Update case_parse.py --- case_parse.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/case_parse.py b/case_parse.py index d687228..b509f48 100644 --- a/case_parse.py +++ b/case_parse.py @@ -23,10 +23,7 @@ class CaseConverter(object): - def __init__(self, detect_acronyms=True, acronyms=None, - toggle_cases={'pascal': 'snake', - 'lower': 'camel', - 'camel': 'pascal'}): + def __init__(self, detect_acronyms=True, acronyms=None, toggle_cases=None): self.detect_acronyms = detect_acronyms if acronyms: self.acronyms = self._clean_acronyms(acronyms)