├── .github └── FUNDING.yml ├── .gitignore ├── BracketHighlighter └── Commands.sublime-commands ├── Close Tag On Slash ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap └── Default (Windows).sublime-keymap ├── Default └── Commands.sublime-commands ├── Edit.py ├── Insert As Tag ├── Commands.sublime-commands ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap └── Default (Windows).sublime-keymap ├── Main.sublime-menu ├── Tag Classes └── Commands.sublime-commands ├── Tag Close Tag ├── Commands.sublime-commands ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap └── Default (Windows).sublime-keymap ├── Tag Lint └── Commands.sublime-commands ├── Tag Package.sublime-settings ├── Tag Remove Attributes └── Commands.sublime-commands ├── Tag Remove └── Commands.sublime-commands ├── Tag.py ├── license.txt ├── readme.md ├── tag_classes.py ├── tag_close_tag.py ├── tag_close_tag_on_slash.py ├── tag_insert_as_tag.py ├── tag_lint.py ├── tag_remove.py └── tag_remove_attributes.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [titoBouzout] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-project 2 | *.cache 3 | push.bat 4 | *.pyc 5 | package-metadata.json -------------------------------------------------------------------------------- /BracketHighlighter/Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | // This just provides an alternative name to https://github.com/facelessuser/BracketHighlighter feature 3 | { 4 | "caption": "Tag: Rename", 5 | "command": "bh_key", 6 | "args": 7 | { 8 | "plugin": 9 | { 10 | "type": ["cfml", "html", "angle"], 11 | "command": "bh_modules.tagnameselect" 12 | } 13 | } 14 | } 15 | ] -------------------------------------------------------------------------------- /Close Tag On Slash/Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["/"], "command": "tag_close_tag_on_slash", 3 | "context": [ 4 | { "key": "preceding_text", "operator": "regex_contains", "operand": "<$", "match_all": true}, 5 | { "key": "setting.is_widget", "operator": "equal", "operand": false } 6 | ] 7 | } 8 | ] -------------------------------------------------------------------------------- /Close Tag On Slash/Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["/"], "command": "tag_close_tag_on_slash", 3 | "context": [ 4 | { "key": "preceding_text", "operator": "regex_contains", "operand": "<$", "match_all": true}, 5 | { "key": "setting.is_widget", "operator": "equal", "operand": false } 6 | ] 7 | } 8 | ] -------------------------------------------------------------------------------- /Close Tag On Slash/Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["/"], "command": "tag_close_tag_on_slash", 3 | "context": [ 4 | { "key": "preceding_text", "operator": "regex_contains", "operand": "<$", "match_all": true}, 5 | { "key": "setting.is_widget", "operator": "equal", "operand": false } 6 | ] 7 | } 8 | ] -------------------------------------------------------------------------------- /Default/Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | //Alternative name for a ST default command 3 | { "caption": "Tag: Fold Attributes", "command": "fold_tag_attributes" } 4 | ] -------------------------------------------------------------------------------- /Edit.py: -------------------------------------------------------------------------------- 1 | # edit.py 2 | # buffer editing for both ST2 and ST3 that "just works" 3 | 4 | import sublime 5 | import sublime_plugin 6 | from collections import defaultdict 7 | 8 | try: 9 | sublime.edit_storage 10 | except AttributeError: 11 | sublime.edit_storage = {} 12 | 13 | class EditStep: 14 | def __init__(self, cmd, *args): 15 | self.cmd = cmd 16 | self.args = args 17 | 18 | def run(self, view, edit): 19 | if self.cmd == 'callback': 20 | return self.args[0](view, edit) 21 | 22 | funcs = { 23 | 'insert': view.insert, 24 | 'erase': view.erase, 25 | 'replace': view.replace, 26 | } 27 | func = funcs.get(self.cmd) 28 | if func: 29 | func(edit, *self.args) 30 | 31 | 32 | class Edit: 33 | defer = defaultdict(dict) 34 | 35 | def __init__(self, view): 36 | self.view = view 37 | self.steps = [] 38 | 39 | def step(self, cmd, *args): 40 | step = EditStep(cmd, *args) 41 | self.steps.append(step) 42 | 43 | def insert(self, point, string): 44 | self.step('insert', point, string) 45 | 46 | def erase(self, region): 47 | self.step('erase', region) 48 | 49 | def replace(self, region, string): 50 | self.step('replace', region, string) 51 | 52 | def callback(self, func): 53 | self.step('callback', func) 54 | 55 | def run(self, view, edit): 56 | for step in self.steps: 57 | step.run(view, edit) 58 | 59 | def __enter__(self): 60 | return self 61 | 62 | def __exit__(self, type, value, traceback): 63 | view = self.view 64 | if sublime.version().startswith('2'): 65 | edit = view.begin_edit() 66 | self.run(edit) 67 | view.end_edit(edit) 68 | else: 69 | key = str(hash(tuple(self.steps))) 70 | sublime.edit_storage[key] = self.run 71 | view.run_command('apply_edit', {'key': key}) 72 | 73 | 74 | class apply_edit(sublime_plugin.TextCommand): 75 | def run(self, edit, key): 76 | sublime.edit_storage.pop(key)(self.view, edit) -------------------------------------------------------------------------------- /Insert As Tag/Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tag: Insert as Tag", 4 | "command": "tag_insert_as_tag" 5 | } 6 | ] -------------------------------------------------------------------------------- /Insert As Tag/Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+shift+,"], 4 | "command": "tag_insert_as_tag" 5 | } 6 | ] -------------------------------------------------------------------------------- /Insert As Tag/Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+shift+,"], 4 | "command": "tag_insert_as_tag" 5 | } 6 | ] -------------------------------------------------------------------------------- /Insert As Tag/Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+shift+,"], 4 | "command": "tag_insert_as_tag" 5 | } 6 | ] -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Edit", 4 | "mnemonic": "E", 5 | "id": "edit", 6 | "children": [ 7 | 8 | { 9 | "caption": "Tag", 10 | "id": "tag", 11 | "children": [ 12 | { 13 | "caption": "-" 14 | }, 15 | { 16 | "command": "tag_insert_as_tag", 17 | "id": "tag-insert-as-tag", 18 | "caption": "Insert as Tag" 19 | }, 20 | { 21 | "command": "tag_classes", 22 | "id": "tag-classes", 23 | "caption": "Get CSS Classes" 24 | }, 25 | { 26 | "caption": "-" 27 | }, 28 | { 29 | "command": "tag_remove_picked_in_selection", 30 | "id": "tag-remove-picked-in-selection", 31 | "caption": "Remove Picked Tags in Selection" 32 | }, 33 | { 34 | "command": "tag_remove_picked_in_document", 35 | "id": "tag-remove-picked-in-document", 36 | "caption": "Remove Picked Tags in Document" 37 | }, 38 | { 39 | "command": "tag_remove_all_in_selection", 40 | "id": "tag-remove-all-in-selection", 41 | "caption": "Remove All Tags in Selection" 42 | }, 43 | { 44 | "command": "tag_remove_all_in_document", 45 | "id": "tag-remove-all-in-document", 46 | "caption": "Remove All Tags in Document" 47 | }, 48 | { 49 | "caption": "-" 50 | }, 51 | { 52 | "command": "tag_remove_picked_attributes_in_selection", 53 | "id": "tag-remove-picked-attributes-in-selection", 54 | "caption": "Remove Picked Attributes From Tags in Selection" 55 | }, 56 | { 57 | "command": "tag_remove_picked_attributes_in_document", 58 | "id": "tag-remove-picked-attributes-in-document", 59 | "caption": "Remove Picked Attributes From Tags in Document" 60 | }, 61 | { 62 | "command": "tag_remove_all_attributes_in_selection", 63 | "id": "tag-remove-all-attributes-in-selection", 64 | "caption": "Remove All Attributes From Tags in Selection" 65 | }, 66 | { 67 | "command": "tag_remove_all_attributes_in_document", 68 | "id": "tag-remove-all-attributes-in-document", 69 | "caption": "Remove All Attributes From Tags in Document" 70 | }, 71 | { 72 | "caption": "-" 73 | }, 74 | { 75 | "command": "tag_lint", 76 | "id": "tag-lint", 77 | "caption": "Tag Lint" 78 | }, 79 | { 80 | "caption": "-" 81 | } 82 | ] 83 | } 84 | ] 85 | }, 86 | { 87 | "caption": "Preferences", 88 | "mnemonic": "n", 89 | "id": "preferences", 90 | "children": [ 91 | { 92 | "caption": "Package Settings", 93 | "mnemonic": "P", 94 | "id": "package-settings", 95 | "children": [ 96 | { 97 | "caption": "Tag", 98 | "children": [ 99 | { 100 | "command": "open_file", 101 | "args": { 102 | "file": "${packages}/Tag/Tag Package.sublime-settings" 103 | }, 104 | "caption": "Settings – Default" 105 | }, 106 | { 107 | "command": "open_file", 108 | "args": { 109 | "file": "${packages}/User/Tag Package.sublime-settings" 110 | }, 111 | "caption": "Settings – User" 112 | } 113 | ] 114 | } 115 | ] 116 | } 117 | ] 118 | } 119 | ] -------------------------------------------------------------------------------- /Tag Classes/Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tag: Get CSS Classes", 4 | "command": "tag_classes" 5 | } 6 | ] -------------------------------------------------------------------------------- /Tag Close Tag/Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tag: Close Tag", 4 | "command": "tag_close_tag" 5 | } 6 | ] -------------------------------------------------------------------------------- /Tag Close Tag/Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["alt+."], "command": "tag_close_tag" } 3 | ] -------------------------------------------------------------------------------- /Tag Close Tag/Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["super+alt+."], "command": "tag_close_tag" } 3 | ] -------------------------------------------------------------------------------- /Tag Close Tag/Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["alt+."], "command": "tag_close_tag" } 3 | ] -------------------------------------------------------------------------------- /Tag Lint/Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tag: Lint Selection or Document", 4 | "command": "tag_lint" 5 | } 6 | ] -------------------------------------------------------------------------------- /Tag Package.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "enable_live_tag_linting": true, 4 | 5 | /* lint these documents */ 6 | "enable_live_tag_linting_document_types": [ 7 | "", "html", "htm", "php", "tpl", "md", "txt", 8 | "xhtml", "xml", "rdf", "xul", "svg", "xsd", "xslt", 9 | "tmTheme", "tmPreferences", "tmLanguage", "sublime-snippet" 10 | ], 11 | 12 | //close tag on slash 13 | "enable_close_tag_on_slash": true, 14 | 15 | //copy css classes to clipboard 16 | "tag_classes_sort": false 17 | 18 | } -------------------------------------------------------------------------------- /Tag Remove Attributes/Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tag: Remove Picked Attributes From Tags in Selection", 4 | "command": "tag_remove_picked_attributes_in_selection" 5 | }, 6 | { 7 | "caption": "Tag: Remove Picked Attributes From Tags in Document", 8 | "command": "tag_remove_picked_attributes_in_document" 9 | }, 10 | { 11 | "caption": "Tag: Remove All Attributes From Tags in Selection", 12 | "command": "tag_remove_all_attributes_in_selection" 13 | }, 14 | { 15 | "caption": "Tag: Remove All Attributes From Tags in Document", 16 | "command": "tag_remove_all_attributes_in_document" 17 | } 18 | ] -------------------------------------------------------------------------------- /Tag Remove/Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tag: Remove Picked Tags in Selection", 4 | "command": "tag_remove_picked_in_selection" 5 | }, 6 | { 7 | "caption": "Tag: Remove Picked Tags in Document", 8 | "command": "tag_remove_picked_in_document" 9 | }, 10 | { 11 | "caption": "Tag: Remove All Tags in Selection", 12 | "command": "tag_remove_all_in_selection" 13 | }, 14 | { 15 | "caption": "Tag: Remove All Tags in Document", 16 | "command": "tag_remove_all_in_document" 17 | } 18 | ] -------------------------------------------------------------------------------- /Tag.py: -------------------------------------------------------------------------------- 1 | import re, sublime 2 | 3 | ST2 = int(sublime.version()) < 3000 4 | 5 | if ST2: 6 | try: 7 | sublime.error_message("TAG Package Message:\n\nThis Package does NOT WORK in Sublime Text 2\n\n Use Sublime Text 3 instead.") 8 | except: 9 | try: 10 | sublime.message_dialog("TAG Package Message:\n\nThis Package does NOT WORK in Sublime Text 2\n\n Use Sublime Text 3 instead.") 11 | except: 12 | pass 13 | 14 | class Tag(): 15 | 16 | def __init__(self): 17 | 18 | Tag.regexp_is_valid = re.compile("^[a-z0-9#\:\-_]+$", re.I); 19 | Tag.regexp_self_closing_optional = re.compile("^<]+/>", re.I); 23 | Tag.xml_files = [item.lower() for item in ['xhtml', 'xml', 'rdf', 'xul', 'svg', 'xsd', 'xslt','tmTheme', 'tmPreferences', 'tmLanguage', 'sublime-snippet', 'opf', 'ncx', 'jsx', 'js']] 24 | 25 | def is_valid(self, content): 26 | return Tag.regexp_is_valid.match(content) 27 | 28 | def is_self_closing(self, content, return_optional_tags = True, is_xml= False): 29 | if return_optional_tags: 30 | if is_xml == False: 31 | return Tag.regexp_self_closing.match(content) or Tag.regexp_is_closing.match(content) 32 | else: 33 | return Tag.regexp_is_closing.match(content) or Tag.regexp_self_closing_xml.match(content) 34 | else: 35 | if is_xml == False: 36 | return Tag.regexp_self_closing_optional.match(content) or Tag.regexp_is_closing.match(content) 37 | else: 38 | return Tag.regexp_is_closing.match(content) or Tag.regexp_self_closing_xml.match(content) 39 | 40 | def name(self, content, return_optional_tags = True, is_xml = False): 41 | if content[:1] == '/' or content[:2] == '\\/': 42 | tag_name = content.split('/')[1].split('>')[0].strip(); 43 | else: 44 | tag_name = content.split(' ')[0].split('>')[0].strip(); 45 | if self.is_valid(tag_name) and not self.is_self_closing(content, return_optional_tags, is_xml): 46 | return tag_name 47 | else: 48 | return '' 49 | 50 | def is_closing(self, content): 51 | if content[:1] == '/' or content[:2] == '\\/' or Tag.regexp_is_closing.match(content): 52 | return True 53 | else: 54 | return False 55 | 56 | def view_is_xml(self, view): 57 | if view.settings().get('is_xml'): 58 | return True 59 | else: 60 | name = view.file_name() 61 | if not name: 62 | is_xml = '') 83 | content += '....' 84 | content += len(tmp.pop(0))*'.' 85 | content += '...' 86 | content += "...".join(tmp) 87 | i += 1 88 | 89 | # multiline line comments /* */ 90 | if content.count('/*') == content.count('*/'): 91 | unparseable = content.split('/*') 92 | content = unparseable.pop(0) 93 | l = len(unparseable) 94 | i = 0 95 | while i < l: 96 | tmp = unparseable[i].split('*/') 97 | content += '..' 98 | content += len(tmp.pop(0))*'.' 99 | content += '..' 100 | content += "..".join(tmp) 101 | i += 1 102 | 103 | # one line comments // 104 | unparseable = re.split('(\s\/\/[^\n]+\n)', content) 105 | for comment in unparseable: 106 | if comment[:3] == '\n//' or comment[:3] == ' //': 107 | content = content.replace(comment, (len(comment))*'.') 108 | 109 | # one line comments # 110 | unparseable = re.split('(\s\#[^\n]+\n)', content) 111 | for comment in unparseable: 112 | if comment[:3] == '\n#' or comment[:3] == ' #': 113 | content = content.replace(comment, (len(comment))*'.') 114 | 115 | # script 116 | if content.count('') 123 | content += '.......' 124 | content += len(tmp.pop(0))*'.' 125 | content += '.........' 126 | content += ".........".join(tmp) 127 | i += 1 128 | 129 | # style 130 | if content.count('') 137 | content += '......' 138 | content += len(tmp.pop(0))*'.' 139 | content += '........' 140 | content += "........".join(tmp) 141 | i += 1 142 | 143 | # here-doc 144 | while '<<<' in content: 145 | content = content.replace('<<<', '...') 146 | while '<<' in content: 147 | content = content.replace('<<', '..') 148 | 149 | return content 150 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | "None are so hopelessly enslaved as those who falsely believe they are free." 2 | Johann Wolfgang von Goethe 3 | 4 | Copyright (C) 2012 Tito Bouzout 5 | 6 | This license apply to all the files inside this program unless noted 7 | different for some files or portions of code inside these files. 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation. http://www.gnu.org/licenses/gpl.html 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see http://www.gnu.org/licenses/gpl.html -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Description 2 | ------------------ 3 | 4 | "Tag" plugin is a collection of packages about tags, mixed together in an effort to provide a single package with utilities to work with tags. 5 | 6 | Currently provides: "Close tag on slash", "Tag Remove", "Insert as Tag", "Tag Remove Attributes", "Tag Close", "Tag Lint" 7 | 8 | Improvements and more features about tags are welcome on this package. Please submit these. 9 | 10 | Close tag on slash 11 | ------------------ 12 | 13 | A command that is meant to be bound to the slash ("/") key in order to semi auto close open HTML tags (in part inspired by the discussion at http://www.sublimetext.com/forum/viewtopic.php?f=5&t=1358). 14 | Requires build 2111 or later of Sublime Text 2. 15 | 16 | *Usage* 17 | 18 | Runs automatically when inserting an Slash "/" into an HTML document. 19 | 20 | Tag Remove 21 | ------------------ 22 | 23 | Provides the ability to remove all tags or some selected tags in a document or in selection(s). 24 | 25 | *Usage* 26 | 27 | The main menu "Edit" -> "Tag" provide access to the commands 28 | 29 | Insert As Tag 30 | ------------------ 31 | 32 | Converts the current word to an html-tag. If there is no current word, a default tag is inserted. 33 | 34 | *Usage* 35 | 36 | The short-cut is "ctrl+shift+," 37 | 38 | Tag Remove Attributes 39 | ------------------ 40 | 41 | Allows to remove attributes from tags for selection or document. 42 | 43 | *Usage* 44 | 45 | The main menu "Edit" -> "Tag" provide access to the commands 46 | 47 | Tag Close 48 | ------------------ 49 | 50 | Overwrite the built-in funtionallity by a custom one provided by this package which fix some bugs. 51 | 52 | *Usage* 53 | 54 | Windows, Linux : ALT+. 55 | OSX : SUPER+ALT+. 56 | 57 | Tag Lint 58 | ------------------ 59 | 60 | Experimental feature which aims to check correctness of opened and closed tags. 61 | 62 | *Usage* 63 | 64 | The main menu "Edit" -> "Tag" -> "Tag Lint" 65 | 66 | 67 | ### Installation 68 | 69 | Download or clone the contents of this repository to a folder named exactly as the package name into the Packages/ folder of ST. 70 | -------------------------------------------------------------------------------- /tag_classes.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import re 3 | 4 | class TagClassesCommand(sublime_plugin.TextCommand): 5 | def run(self, edit): 6 | data = '' 7 | classes = [] 8 | for region in self.view.sel(): 9 | if region.empty(): 10 | continue 11 | data += self.view.substr(sublime.Region(region.begin(), region.end())) 12 | 13 | if not data: 14 | data += self.view.substr(sublime.Region(0, self.view.size())) 15 | 16 | if data: 17 | re_classes = (" ".join(re.compile('class="([^"]+)"').findall(data))).split() 18 | for item in re_classes: 19 | item = item.strip() 20 | if '.'+item+' {}' not in classes: 21 | classes.append('.'+item+' {}') 22 | if classes: 23 | s = sublime.load_settings('Tag Package.sublime-settings') 24 | if s.get('tag_classes_sort', False): 25 | classes.sort() 26 | sublime.set_clipboard("\n".join(classes)) 27 | sublime.status_message("CSS Classes Copied to Clipboard") 28 | 29 | 30 | -------------------------------------------------------------------------------- /tag_close_tag.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | from Tag import Tag 3 | 4 | Tag = Tag.Tag() 5 | 6 | class TagCloseTagCommand(sublime_plugin.TextCommand): 7 | 8 | def run(self, edit): 9 | 10 | view = self.view 11 | is_xml = Tag.view_is_xml(view); 12 | 13 | closed_some_tag = False 14 | new_selections = [] 15 | new_selections_insert = [] 16 | 17 | for region in view.sel(): 18 | cursorPosition = region.begin() 19 | 20 | tag = self.close_tag(view.substr(sublime.Region(0, cursorPosition)), is_xml) 21 | 22 | if tag and tag != '" to allow 45 | # to the application indent these tags correctly 46 | if closed_some_tag: 47 | view.run_command('hide_auto_complete') 48 | for sel in new_selections_insert: 49 | view.sel().add(sel) 50 | view.run_command('insert', {"characters": ">"}) 51 | view.run_command('reindent', {"force_indent": True}) 52 | 53 | for sel in new_selections: 54 | view.sel().add(sel) 55 | 56 | def close_tag(self, data, is_xml): 57 | 58 | data = Tag.clean_html(data).split('<') 59 | data.reverse() 60 | 61 | try: 62 | i = 0 63 | lenght = len(data)-1 64 | while i < lenght: 65 | tag = Tag.name(data[i], True, is_xml) 66 | # if opening tag, close the tag 67 | if tag: 68 | if not Tag.is_closing(data[i]): 69 | return '" to allow 55 | # to the application indent these tags correctly 56 | if closed_some_tag: 57 | view.run_command('hide_auto_complete') 58 | for sel in new_selections_insert: 59 | view.sel().add(sel) 60 | view.run_command('insert', {"characters": ">"}) 61 | view.run_command('reindent', {"force_indent": True}) 62 | 63 | for sel in new_selections: 64 | view.sel().add(sel) 65 | 66 | def close_tag(self, data, is_xml): 67 | 68 | data = Tag.clean_html(data).split('<') 69 | data.reverse() 70 | data.pop(0); 71 | 72 | try: 73 | i = 0 74 | lenght = len(data)-1 75 | while i < lenght: 76 | tag = Tag.name(data[i], True, is_xml) 77 | # if opening tag, close the tag 78 | if tag and not Tag.is_closing(data[i]): 79 | return '/'+Tag.name(data[i], True, is_xml)+'' 80 | # if closing tag, jump to opening tag 81 | else: 82 | if tag: 83 | i = i+1 84 | skip = 0 85 | while i < lenght: 86 | if Tag.name(data[i], True, is_xml) == tag: 87 | if not Tag.is_closing(data[i]): 88 | if skip == 0: 89 | break 90 | else: 91 | skip = skip-1 92 | else: 93 | skip = skip+1 94 | i = i+1 95 | i = i+1 96 | return '/' 97 | except: 98 | return '/'; -------------------------------------------------------------------------------- /tag_insert_as_tag.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin, re 2 | from Tag import Tag 3 | 4 | Tag = Tag.Tag() 5 | 6 | class TagInsertAsTagCommand(sublime_plugin.TextCommand): 7 | 8 | def run(self, edit): 9 | view = self.view 10 | new_selections = [] 11 | for region in view.sel(): 12 | source = view.substr(region) 13 | if not source.strip(): 14 | region = view.word(region) 15 | source = view.substr(region) 16 | if not source.strip(): 17 | new_selections.append(sublime.Region(region.a, region.b)) 18 | pass 19 | else: 20 | if re.match("^\s", source): 21 | view.replace(edit, region, '

'+source+'

') 22 | new_selections.append(sublime.Region(region.end()+3, region.end()+3)) 23 | elif Tag.is_self_closing(source): 24 | view.replace(edit, region, '<'+source+'/>') 25 | new_selections.append(sublime.Region(region.end()+3, region.end()+3)) 26 | else: 27 | tag = source.split('\r')[0].split('\n')[0].split(' ')[0] 28 | if tag and Tag.is_valid(tag) and tag != '<' and tag != '': 29 | view.replace(edit, region, '<'+source+'>') 30 | new_selections.append(sublime.Region(region.end()+2, region.end()+2)) 31 | else: 32 | new_selections.append(sublime.Region(region.end(), region.end())) 33 | 34 | view.sel().clear() 35 | for sel in new_selections: 36 | view.sel().add(sel) -------------------------------------------------------------------------------- /tag_lint.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | from time import time, sleep 3 | import threading 4 | import _thread as thread 5 | from Tag import Tag 6 | import re 7 | 8 | Tag = Tag.Tag() 9 | def plugin_loaded(): 10 | global s, Pref, tag_lint, tag_lint_run 11 | s = sublime.load_settings('Tag Package.sublime-settings') 12 | Pref = Pref(); 13 | Pref.load() 14 | s.clear_on_change('reload') 15 | s.add_on_change('reload', lambda:Pref.load()) 16 | tag_lint = TagLint(); 17 | tag_lint_run = tag_lint.run 18 | if not 'running_tag_lint_loop' in globals(): 19 | global running_tag_lint_loop 20 | running_tag_lint_loop = True 21 | thread.start_new_thread(tag_lint_loop, ()) 22 | 23 | class Pref: 24 | def load(self): 25 | Pref.view = False 26 | Pref.modified = False 27 | Pref.elapsed_time = 0.4 28 | Pref.time = time() 29 | Pref.wait_time = 0.8 30 | Pref.running = False 31 | Pref.enable_live_tag_linting = s.get('enable_live_tag_linting', True) 32 | Pref.hard_highlight = ['', 'html', 'htm', 'php', 'tpl', 'md', 'txt', 'jsx', 'js'] 33 | Pref.enable_live_tag_linting_document_types = [item.lower() for item in s.get('enable_live_tag_linting_document_types', '')] 34 | Pref.statuses = 0 35 | Pref.message_line = -1 36 | Pref.selection_last_line = -1 37 | Pref.message = '' 38 | Pref.view_size = 0 39 | 40 | class TagLint(sublime_plugin.EventListener): 41 | 42 | def on_activated(self, view): 43 | if not view.settings().get('is_widget') and not view.is_scratch(): 44 | Pref.view = view 45 | Pref.selection_last_line = -1 46 | 47 | def on_load(self, view): 48 | if not view.settings().get('is_widget') and not view.is_scratch(): 49 | Pref.modified = True 50 | Pref.view = view 51 | sublime.set_timeout(lambda:self.run(True), 0) 52 | 53 | def on_modified(self, view): 54 | if not view.settings().get('is_widget') and not view.is_scratch(): 55 | Pref.modified = True 56 | Pref.time = time() 57 | 58 | def on_selection_modified(self, view): 59 | if Pref.enable_live_tag_linting: 60 | sel = view.sel() 61 | if sel and Pref.message_line != -1 and Pref.message != '': 62 | line = view.rowcol(sel[0].end())[0] 63 | if Pref.selection_last_line != line: 64 | Pref.selection_last_line = line 65 | if line == Pref.message_line: 66 | view.set_status('TagLint', Pref.message) 67 | else: 68 | Pref.statuses += 1 69 | sublime.set_timeout(lambda:self.clear_status(view, False), 7000); 70 | #view.erase_status('TagLint') 71 | # else: 72 | # view.erase_status('TagLint') 73 | 74 | def on_close(self, view): 75 | Pref.view = False 76 | Pref.modified = True 77 | 78 | def guess_view(self): 79 | if sublime.active_window() and sublime.active_window().active_view(): 80 | Pref.view = sublime.active_window().active_view() 81 | 82 | def run(self, asap = False, from_command = False): 83 | now = time() 84 | if asap == False and (now - Pref.time < Pref.wait_time): 85 | return 86 | if (Pref.enable_live_tag_linting or from_command) and Pref.modified and not Pref.running: 87 | Pref.modified = False 88 | if from_command: 89 | Pref.view = sublime.active_window().active_view() 90 | if Pref.view: 91 | view = Pref.view 92 | Pref.view_size = view.size() 93 | if Pref.view_size > 10485760: 94 | return 95 | if Pref.view.settings().get('is_widget') or Pref.view.is_scratch(): 96 | return 97 | file_ext = ('name.'+(view.file_name() or '')).split('.') 98 | file_ext.reverse() 99 | file_ext = file_ext.pop(0).lower() 100 | if not from_command and file_ext not in Pref.enable_live_tag_linting_document_types: 101 | return 102 | Pref.running = True 103 | is_xml = Tag.view_is_xml(view) 104 | if from_command: 105 | if view.sel(): 106 | region = view.sel()[0] 107 | if region.empty(): 108 | region = sublime.Region(0, view.size()) 109 | else: 110 | region = sublime.Region(0, view.size()) 111 | else: 112 | region = sublime.Region(0, view.size()) 113 | original_position = region.begin() 114 | content = view.substr(region) 115 | TagLintThread(view, content, original_position, is_xml, from_command).start() 116 | else: 117 | self.guess_view() 118 | 119 | def display(self, view, message, invalid_tag_located_at, from_command): 120 | if view is not None: 121 | view.erase_regions("TagLint") 122 | if invalid_tag_located_at > -1: 123 | invalid_tag_located_at_start = invalid_tag_located_at 124 | invalid_tag_located_at_end = invalid_tag_located_at+1 125 | size = view.size() 126 | while invalid_tag_located_at_end < size: 127 | end = view.substr(sublime.Region(invalid_tag_located_at_end, invalid_tag_located_at_end+1)) 128 | if end == '>': 129 | invalid_tag_located_at_end += 1 130 | break 131 | elif end == '<': 132 | break; 133 | invalid_tag_located_at_end += 1 134 | if invalid_tag_located_at_start - invalid_tag_located_at_end > 100: 135 | break 136 | region = sublime.Region(invalid_tag_located_at_start, invalid_tag_located_at_end) 137 | line, col = view.rowcol(region.a); 138 | string = re.split('\s|>', view.substr(region))[0] 139 | if len(string) > 1 and string[1] == '/': 140 | mas = 2 141 | else: 142 | mas = 1 143 | region = sublime.Region(region.a+mas, region.a+len(string)) 144 | view.add_regions("TagLint", [region], 'variable.parameter', 'dot', sublime.PERSISTENT | sublime.DRAW_SQUIGGLY_UNDERLINE | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_EMPTY_AS_OVERWRITE) 145 | Pref.message_line = line 146 | Pref.message = message 147 | view.set_status('TagLint', Pref.message+' in Line '+str(Pref.message_line+1)+' ') 148 | Pref.statuses += 1 149 | sublime.set_timeout(lambda:self.clear_status(view, from_command), 7000); 150 | if from_command: 151 | view.show_at_center(region) 152 | else: 153 | Pref.message_line = -1 154 | Pref.message = '' 155 | if from_command: 156 | view.set_status('TagLint', 'No errors found') 157 | Pref.statuses += 1 158 | sublime.set_timeout(lambda:self.clear_status(view, from_command), 7000); 159 | else: 160 | view.erase_status('TagLint') 161 | else: 162 | Pref.message_line = -1 163 | Pref.message = '' 164 | Pref.running = False 165 | 166 | def clear_status(self, view, from_command): 167 | Pref.statuses -= 1 168 | if view is not None and Pref.statuses == 0: 169 | view.erase_status('TagLint') 170 | if from_command and Pref.enable_live_tag_linting == False: 171 | view.erase_regions("TagLint") 172 | 173 | 174 | 175 | class TagLintThread(threading.Thread): 176 | 177 | def __init__(self, view, content, original_position, is_xml, from_command): 178 | threading.Thread.__init__(self) 179 | self.view = view 180 | self.content = content 181 | self.original_position = original_position 182 | self.is_xml = is_xml 183 | self.message = '' 184 | self.invalid_tag_located_at = -1 185 | self.from_command = from_command 186 | 187 | def run(self): 188 | 189 | begin = time() 190 | 191 | content = self.content 192 | original_position = self.original_position 193 | is_xml = self.is_xml 194 | 195 | # remove unparseable content 196 | 197 | content = Tag.clean_html(content) 198 | 199 | # linting: opening tags 200 | 201 | data = content.split('<') 202 | 203 | position = original_position+len(data.pop(0)) 204 | 205 | invalid_tag_located_at = -1 206 | 207 | i = 0 208 | lenght = len(data) 209 | first_at = 0 210 | while i < lenght: 211 | tag = Tag.name(data[i], False, is_xml) 212 | if tag and tag != 'html' and tag != 'body' and tag != 'head': 213 | # if opening tag, then check if closing tag exists 214 | if not Tag.is_closing(data[i]): 215 | # print tag+' is opening ' 216 | if first_at == 0: 217 | first_at = position 218 | a = i+1 219 | skip = 0 220 | while a < lenght: 221 | inner_tag_name = Tag.name(data[a], False, is_xml) 222 | # check if same tag was found 223 | if inner_tag_name and inner_tag_name == tag: 224 | # check if tag is closing 225 | if Tag.is_closing(data[a]): 226 | if skip == 0: 227 | break 228 | else: 229 | skip = skip-1 230 | else: 231 | skip = skip+1 232 | a = a+1 233 | if a >= lenght: 234 | self.message = '"'+tag+'" tag is not closing' 235 | invalid_tag_located_at = position 236 | break 237 | position += len(data[i])+1 238 | i = i+1 239 | 240 | # linting: closing tags 241 | 242 | if invalid_tag_located_at == -1: 243 | 244 | position = original_position+len(content); 245 | 246 | data = content.split('<') 247 | data.reverse() 248 | 249 | i = 0 250 | lenght = len(data)-1 251 | while i < lenght: 252 | tag = Tag.name(data[i], False, is_xml) 253 | if tag and tag != 'html' and tag != 'body' and tag != 'head': 254 | # if closing tag, check if opening tag exists 255 | if Tag.is_closing(data[i]): 256 | # print tag+' is closing ' 257 | a = i+1 258 | skip = 0 259 | while a < lenght: 260 | inner_tag_name = Tag.name(data[a], False, is_xml) 261 | if inner_tag_name and inner_tag_name == tag: 262 | # check if tag is opening 263 | if not Tag.is_closing(data[a]): 264 | if skip == 0: 265 | break 266 | else: 267 | skip = skip-1 268 | else: 269 | skip = skip+1 270 | a = a+1 271 | if a >= lenght: 272 | self.message = '"'+tag+'" tag is not opening' 273 | invalid_tag_located_at = position-(len(data[i])+1) 274 | if invalid_tag_located_at < first_at: 275 | invalid_tag_located_at = -1 276 | break 277 | position -= len(data[i])+1 278 | i = i+1 279 | 280 | elapsed_time = time() - begin; 281 | 282 | # print 'Benchmark: '+str(elapsed_time) 283 | 284 | self.invalid_tag_located_at = invalid_tag_located_at 285 | 286 | sublime.set_timeout(lambda:tag_lint.display(self.view, self.message, self.invalid_tag_located_at, self.from_command), 0) 287 | 288 | 289 | 290 | def tag_lint_loop(): 291 | while True: 292 | # sleep time is adaptive, if takes more than 0.4 to calculate the word count 293 | # sleep_time becomes elapsed_time*3 294 | if Pref.running == False: 295 | sublime.set_timeout(lambda:tag_lint_run(), 0) 296 | sleep((Pref.elapsed_time*3 if Pref.elapsed_time > 0.4 else 0.4)) 297 | 298 | 299 | 300 | 301 | class TagLintCommand(sublime_plugin.WindowCommand): 302 | def run(self): 303 | Pref.modified = True 304 | Pref.running = False 305 | tag_lint_run(True, True); -------------------------------------------------------------------------------- /tag_remove.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import re 3 | from .Edit import Edit as Edit 4 | 5 | def TagRemoveAll(data, view): 6 | return re.sub(r'<[^\?][^>]*>', '', data); 7 | 8 | def TagRemoveSelected(data, tags, view): 9 | tags = tags.replace(',', ' ').replace(';', ' ').replace('|', ' ').replace('<', ' ').replace('>', ' ')+' ' 10 | for tag in tags.split(' '): 11 | if tag: 12 | regexp = re.compile('<'+re.escape(tag)+'(| [^>]*)>', re.IGNORECASE) 13 | data = regexp.sub('', data); 14 | regexp = re.compile('', re.IGNORECASE) 15 | data = regexp.sub('', data); 16 | return data; 17 | 18 | class TagRemoveAllInSelectionCommand(sublime_plugin.TextCommand): 19 | def run(self, edit): 20 | for region in self.view.sel(): 21 | if region.empty(): 22 | continue 23 | dataRegion = sublime.Region(region.begin(), region.end()) 24 | data = TagRemoveAll(self.view.substr(dataRegion), self.view) 25 | self.view.replace(edit, dataRegion, data); 26 | 27 | class TagRemoveAllInDocumentCommand(sublime_plugin.TextCommand): 28 | def run(self, edit): 29 | dataRegion = sublime.Region(0, self.view.size()) 30 | data = TagRemoveAll(self.view.substr(dataRegion), self.view) 31 | self.view.replace(edit, dataRegion, data); 32 | 33 | class TagRemovePickedInSelectionCommand(sublime_plugin.TextCommand): 34 | def run(self, edit, tags = False): 35 | if not tags: 36 | import functools 37 | self.view.window().run_command('hide_panel'); 38 | self.view.window().show_input_panel("Remove the following tags:", '', functools.partial(self.on_done, edit), None, None) 39 | else: 40 | self.on_done(edit, tags); 41 | 42 | def on_done(self, edit, tags): 43 | for region in self.view.sel(): 44 | if region.empty(): 45 | continue 46 | dataRegion = sublime.Region(region.begin(), region.end()) 47 | data = TagRemoveSelected(self.view.substr(dataRegion), tags, self.view) 48 | with Edit(self.view) as edit: 49 | edit.replace(dataRegion, data); 50 | 51 | class TagRemovePickedInDocumentCommand(sublime_plugin.TextCommand): 52 | def run(self, edit, tags = False): 53 | if not tags: 54 | import functools 55 | self.view.window().run_command('hide_panel'); 56 | self.view.window().show_input_panel("Remove the following tags:", '', functools.partial(self.on_done, edit), None, None) 57 | else: 58 | self.on_done(edit, tags); 59 | 60 | def on_done(self, edit, tags): 61 | dataRegion = sublime.Region(0, self.view.size()) 62 | data = TagRemoveSelected(self.view.substr(dataRegion), tags, self.view) 63 | with Edit(self.view) as edit: 64 | edit.replace(dataRegion, data); 65 | -------------------------------------------------------------------------------- /tag_remove_attributes.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import re 3 | from .Edit import Edit as Edit 4 | 5 | def TagRemoveAttributesClean(data): 6 | regexp = re.compile('(<([a-z0-9\:\-_]+)\s+>)'); 7 | data = regexp.sub('<\\2>', data); 8 | return data 9 | 10 | def TagRemoveAttributesAll(data, view): 11 | return TagRemoveAttributesClean(re.sub('(<([a-z0-9\:\-_]+)\s+[^>]+>)', '<\\2>', data)); 12 | 13 | def TagRemoveAttributesSelected(data, attributes, view): 14 | attributes = attributes.replace(',', ' ').replace(';', ' ').replace('|', ' ')+' ' 15 | for attribute in attributes.split(' '): 16 | if attribute: 17 | regexp = re.compile('(<([a-z0-9\:\-_]+\s+)([^>]*)\s*'+re.escape(attribute)+'="[^"]+"\s*([^>]*)>)') 18 | data = regexp.sub('<\\2\\3\\4>', data); 19 | data = TagRemoveAttributesClean(data); 20 | return data; 21 | 22 | class TagRemoveAllAttributesInSelectionCommand(sublime_plugin.TextCommand): 23 | def run(self, edit): 24 | for region in self.view.sel(): 25 | if region.empty(): 26 | continue 27 | dataRegion = sublime.Region(region.begin(), region.end()) 28 | data = TagRemoveAttributesAll(self.view.substr(dataRegion), self.view) 29 | self.view.replace(edit, dataRegion, data); 30 | 31 | class TagRemoveAllAttributesInDocumentCommand(sublime_plugin.TextCommand): 32 | def run(self, edit): 33 | dataRegion = sublime.Region(0, self.view.size()) 34 | data = TagRemoveAttributesAll(self.view.substr(dataRegion), self.view) 35 | self.view.replace(edit, dataRegion, data); 36 | 37 | class TagRemovePickedAttributesInSelectionCommand(sublime_plugin.TextCommand): 38 | def run(self, edit, attributes = False): 39 | if not attributes: 40 | import functools 41 | self.view.window().run_command('hide_panel'); 42 | self.view.window().show_input_panel("Remove the following attributes:", '', functools.partial(self.on_done, edit), None, None) 43 | else: 44 | self.on_done(edit, attributes) 45 | 46 | def on_done(self, edit, attributes): 47 | for region in self.view.sel(): 48 | if region.empty(): 49 | continue 50 | dataRegion = sublime.Region(region.begin(), region.end()) 51 | data = TagRemoveAttributesSelected(self.view.substr(dataRegion), attributes, self.view) 52 | with Edit(self.view) as edit: 53 | edit.replace(dataRegion, data); 54 | 55 | class TagRemovePickedAttributesInDocumentCommand(sublime_plugin.TextCommand): 56 | def run(self, edit, attributes = False): 57 | if not attributes: 58 | import functools 59 | self.view.window().run_command('hide_panel'); 60 | self.view.window().show_input_panel("Remove the following attributes:", '', functools.partial(self.on_done, edit), None, None) 61 | else: 62 | self.on_done(edit, attributes) 63 | 64 | def on_done(self, edit, attributes): 65 | dataRegion = sublime.Region(0, self.view.size()) 66 | data = TagRemoveAttributesSelected(self.view.substr(dataRegion), attributes, self.view) 67 | with Edit(self.view) as edit: 68 | edit.replace(dataRegion, data); --------------------------------------------------------------------------------