├── libs ├── __init__.py └── cssformatter.py ├── messages ├── 1.0.7.txt ├── 1.0.4.txt ├── 1.0.2.txt ├── 1.0.5.txt ├── 1.0.8.txt ├── 1.0.1.txt ├── 1.0.6.txt ├── 1.0.3.txt └── install.txt ├── messages.json ├── CSS Format.sublime-settings ├── Context.sublime-menu ├── LICENSE ├── Default.sublime-commands ├── Example.sublime-keymap ├── Main.sublime-menu ├── css_format.py └── README.md /libs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /messages/1.0.7.txt: -------------------------------------------------------------------------------- 1 | CSS Format 1.0.7 Changelog: 2 | 3 | Optimize 4 | - Support LESS merge. 5 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "1.0.8": "messages/1.0.8.txt" 4 | } 5 | -------------------------------------------------------------------------------- /messages/1.0.4.txt: -------------------------------------------------------------------------------- 1 | CSS Format 1.0.4 Changelog: 2 | 3 | New features 4 | - Format on save. 5 | - Config Indentation. 6 | -------------------------------------------------------------------------------- /messages/1.0.2.txt: -------------------------------------------------------------------------------- 1 | CSS Format 1.0.2 Changelog: 2 | 3 | Fixed bug 4 | - Wrap after @charset. 5 | - Fix space after ; which in data url. 6 | -------------------------------------------------------------------------------- /messages/1.0.5.txt: -------------------------------------------------------------------------------- 1 | CSS Format 1.0.5 Changelog: 2 | 3 | Fixed bug 4 | - Protect strings in properties. 5 | - Optimize comments' format. 6 | - Optimize commas' format. -------------------------------------------------------------------------------- /messages/1.0.8.txt: -------------------------------------------------------------------------------- 1 | CSS Format 1.0.8 Changelog: 2 | 3 | Merge pull request 4 | - #34 Added "Rule Break" 5 | - #51 Add newline at the end of file, resolve issue #40 and #50 6 | 7 | Add MIT LICENSE -------------------------------------------------------------------------------- /messages/1.0.1.txt: -------------------------------------------------------------------------------- 1 | CSS Format 1.0.1 Changelog: 2 | 3 | New features 4 | - Wrap after commas in selector. 5 | - Indent for SASS/SCSS/LESS. 6 | 7 | Fixed bug 8 | - Indent in @media and @keyframes. 9 | -------------------------------------------------------------------------------- /messages/1.0.6.txt: -------------------------------------------------------------------------------- 1 | CSS Format 1.0.6 Changelog: 2 | 3 | Fixed bug 4 | - Properties' indentation in comments. 5 | - Do not break arguments. 6 | 7 | Optimize 8 | - Add a blank line between each block in `expand-bs` mode. 9 | -------------------------------------------------------------------------------- /messages/1.0.3.txt: -------------------------------------------------------------------------------- 1 | CSS Format 1.0.3 Changelog: 2 | 3 | New features 4 | - Add options for Expanded and Compact style. 5 | - Remove the last semicolon before } when Compress CSS. 6 | 7 | Fixed bug 8 | - Special characters in the URL will be processed. 9 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | Thank you for installing CSS Format! 2 | 3 | 4 | Usage 5 | ----- 6 | 7 | Select the code, or place cursor in the document, and execute commands in one of the following ways: 8 | 9 | * Context Menu: CSS Format. 10 | * Edit Menu: Edit > CSS Format. 11 | * Command Panel: Open command panel: `Ctrl+Shift+P` (Linux/Windows) or `Cmd+Shift+P` (OS X) and select **CSS Format: XXX**. 12 | 13 | 14 | Shortcuts 15 | --------- 16 | 17 | By default CSS Format provides no keyboard shortcuts to avoid conflicts, but you can view the included `Example.sublime-keymaps` file to get an idea how to set up your own. 18 | 19 | 20 | Author 21 | ------ 22 | 23 | Created by Mutian [http://mutian.wang/]. 24 | For more info, you can send email to me: mutian@me.com! 25 | -------------------------------------------------------------------------------- /CSS Format.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Code Indentation 3 | // "\t" : 1 tab 4 | // " " : 2 spaces 5 | // " " : 4 spaces 6 | "indentation": "\t", 7 | 8 | // Block Break (Expand Mode Only) 9 | // "\n" : 1 line break after each rules block 10 | // "\n\n" : 2 line breaks after each rules block 11 | "expand_block_break": "\n\n", 12 | 13 | // Format on Save 14 | // false : disabled 15 | // true : enabled 16 | "format_on_save": false, 17 | 18 | // On Save Action 19 | // "expand" : Expanded 20 | // "expand-bs" : Expanded (Break Selectors) 21 | // "compact" : Compact 22 | // "compact-ns" : Compact (No Spaces) 23 | // "compact-bs" : Compact (Break Selectors) 24 | // "compact-bs-ns" : Compact (Break Selectors, No Spaces) 25 | // "compress" : Compressed 26 | "format_on_save_action": "expand", 27 | 28 | // On Save File Filter 29 | // Please use regular expression 30 | "format_on_save_filter": "\\.(css|sass|scss|less)$" 31 | } -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "-" }, 3 | { 4 | "id": "css-format", 5 | "caption": "CSS Format", 6 | "children": 7 | [ 8 | { 9 | "caption": "Expanded", 10 | "command": "css_format", 11 | "args": { "action": "expand"} 12 | }, 13 | { 14 | "caption": "Expanded (Break Selectors)", 15 | "command": "css_format", 16 | "args": { "action": "expand-bs"} 17 | }, 18 | { 19 | "caption": "Compact", 20 | "command": "css_format", 21 | "args": { "action": "compact" } 22 | }, 23 | { 24 | "caption": "Compact (No Spaces)", 25 | "command": "css_format", 26 | "args": { "action": "compact-ns" } 27 | }, 28 | { 29 | "caption": "Compact (Break Selectors)", 30 | "command": "css_format", 31 | "args": { "action": "compact-bs" } 32 | }, 33 | { 34 | "caption": "Compact (Break Selectors, No Spaces)", 35 | "command": "css_format", 36 | "args": { "action": "compact-bs-ns" } 37 | }, 38 | { 39 | "caption": "Compressed", 40 | "command": "css_format", 41 | "args": { "action": "compress" } 42 | } 43 | ] 44 | } 45 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2017 Mutian Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | // Convert to Expanded Format 3 | { 4 | "caption": "Format CSS: Expanded", 5 | "command": "css_format", 6 | "args": { 7 | "action": "expand" 8 | } 9 | }, 10 | 11 | // Convert to Expanded Format (Break Selectors) 12 | { 13 | "caption": "Format CSS: Expanded (Break Selectors)", 14 | "command": "css_format", 15 | "args": { 16 | "action": "expand-bs" 17 | } 18 | }, 19 | 20 | // Convert to Compact Format 21 | { 22 | "caption": "Format CSS: Compact", 23 | "command": "css_format", 24 | "args": { 25 | "action": "compact" 26 | } 27 | }, 28 | 29 | // Convert to Compact Format (No Spaces) 30 | { 31 | "caption": "Format CSS: Compact (No Spaces)", 32 | "command": "css_format", 33 | "args": { 34 | "action": "compact-ns" 35 | } 36 | }, 37 | 38 | // Convert to Compact Format (Break Selectors) 39 | { 40 | "caption": "Format CSS: Compact (Break Selectors)", 41 | "command": "css_format", 42 | "args": { 43 | "action": "compact-bs" 44 | } 45 | }, 46 | 47 | // Convert to Compact Format (Break Selectors and No Spaces) 48 | { 49 | "caption": "Format CSS: Compact (Break Selectors, No Spaces)", 50 | "command": "css_format", 51 | "args": { 52 | "action": "compact-bs-ns" 53 | } 54 | }, 55 | 56 | // Convert to Compressed Format 57 | { 58 | "caption": "Format CSS: Compressed", 59 | "command": "css_format", 60 | "args": { 61 | "action": "compress" 62 | } 63 | } 64 | ] -------------------------------------------------------------------------------- /Example.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | // Convert to Expanded Format 3 | { 4 | "keys": ["ctrl+alt+["], 5 | "command": "css_format", 6 | "args": { 7 | "action": "expand" 8 | } 9 | }, 10 | 11 | // Convert to expanded format (break selectors) 12 | { 13 | "keys": ["ctrl+alt+b"], 14 | "command": "css_format", 15 | "args": { 16 | "action": "expand-bs" 17 | } 18 | }, 19 | 20 | // Convert to Expanded Format (Break Selectors) 21 | { 22 | "keys": ["ctrl+alt+v"], 23 | "command": "css_format", 24 | "args": { 25 | "action": "expand-bs" 26 | } 27 | }, 28 | 29 | // Convert to Compact Format 30 | { 31 | "keys": ["ctrl+alt+]"], 32 | "command": "css_format", 33 | "args": { 34 | "action": "compact" 35 | } 36 | }, 37 | 38 | // Convert to Compact Format (No Spaces) 39 | { 40 | "keys": ["ctrl+alt+n"], 41 | "command": "css_format", 42 | "args": { 43 | "action": "compact-ns" 44 | } 45 | }, 46 | 47 | // Convert to Compact Format (Break Selectors) 48 | { 49 | "keys": ["ctrl+alt+b"], 50 | "command": "css_format", 51 | "args": { 52 | "action": "compact-bs" 53 | } 54 | }, 55 | 56 | // Convert to Compact Format (Break Selectors and No Spaces) 57 | { 58 | "keys": ["ctrl+alt+m"], 59 | "command": "css_format", 60 | "args": { 61 | "action": "compact-bs-ns" 62 | } 63 | }, 64 | 65 | // Convert to Compressed Format 66 | { 67 | "keys": ["ctrl+alt+\\"], 68 | "command": "css_format", 69 | "args": { 70 | "action": "compress" 71 | } 72 | } 73 | ] -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "edit", 4 | "children": 5 | [ 6 | { 7 | "caption": "-" 8 | }, 9 | { 10 | "id": "css_format", 11 | "caption": "CSS Format", 12 | "children": 13 | [ 14 | { 15 | "caption": "Expanded", 16 | "command": "css_format", 17 | "args": { "action": "expand"} 18 | }, 19 | { 20 | "caption": "Expanded (Break Selectors)", 21 | "command": "css_format", 22 | "args": { "action": "expand-bs"} 23 | }, 24 | { 25 | "caption": "Compact", 26 | "command": "css_format", 27 | "args": { "action": "compact" } 28 | }, 29 | { 30 | "caption": "Compact (No Spaces)", 31 | "command": "css_format", 32 | "args": { "action": "compact-ns" } 33 | }, 34 | { 35 | "caption": "Compact (Break Selectors)", 36 | "command": "css_format", 37 | "args": { "action": "compact-bs" } 38 | }, 39 | { 40 | "caption": "Compact (Break Selectors, No Spaces)", 41 | "command": "css_format", 42 | "args": { "action": "compact-bs-ns" } 43 | }, 44 | { 45 | "caption": "Compressed", 46 | "command": "css_format", 47 | "args": { "action": "compress" } 48 | } 49 | ] 50 | } 51 | ] 52 | }, 53 | { 54 | "id": "preferences", 55 | "children": 56 | [ 57 | { 58 | "id": "package-settings", 59 | "children": 60 | [ 61 | { 62 | "caption": "CSS Format", 63 | "children": 64 | [ 65 | { 66 | "command": "open_file", 67 | "args": {"file": "${packages}/CSS Format/README.md"}, 68 | "caption": "README" 69 | }, 70 | { "caption": "-" }, 71 | { 72 | "command": "open_file", 73 | "args": {"file": "${packages}/CSS Format/CSS Format.sublime-settings"}, 74 | "caption": "Settings – Default" 75 | }, 76 | { 77 | "command": "open_file", 78 | "args": {"file": "${packages}/User/CSS Format.sublime-settings"}, 79 | "caption": "Settings – User" 80 | }, 81 | { "caption": "-" }, 82 | { 83 | "command": "open_file", 84 | "args": { 85 | "file": "${packages}/CSS Format/Example.sublime-keymap" 86 | }, 87 | "caption": "Key Bindings – Example" 88 | }, 89 | { 90 | "command": "open_file", 91 | "args": { 92 | "file": "${packages}/User/Default (Windows).sublime-keymap", 93 | "platform": "Windows" 94 | }, 95 | "caption": "Key Bindings – User" 96 | }, 97 | { 98 | "command": "open_file", 99 | "args": { 100 | "file": "${packages}/User/Default (OSX).sublime-keymap", 101 | "platform": "OSX" 102 | }, 103 | "caption": "Key Bindings – User" 104 | }, 105 | { 106 | "command": "open_file", 107 | "args": { 108 | "file": "${packages}/User/Default (Linux).sublime-keymap", 109 | "platform": "Linux" 110 | }, 111 | "caption": "Key Bindings – User" 112 | }, 113 | { "caption": "-" } 114 | ] 115 | } 116 | ] 117 | } 118 | ] 119 | } 120 | ] 121 | -------------------------------------------------------------------------------- /css_format.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # CSS Formatter for Sublime Text 5 | # 6 | # Author: Mutian Wang 7 | 8 | import sublime, sublime_plugin, sys, os, re 9 | 10 | if sys.version_info < (3, 0): 11 | # ST2, Python 2.6 12 | from libs.cssformatter import CssFormater 13 | else: 14 | # ST3, Python 3.3 15 | from .libs.cssformatter import CssFormater 16 | 17 | 18 | class CssFormatCommand(sublime_plugin.TextCommand): 19 | 20 | def run(self, edit, action='compact', detectSel=True): 21 | view = self.view 22 | 23 | if view.is_loading(): 24 | sublime.status_message('Waiting for loading.') 25 | return False 26 | 27 | # load settings 28 | global_settings = sublime.load_settings('CSS Format.sublime-settings') 29 | indentation = view.settings().get('indentation', global_settings.get('indentation', '\t')) 30 | expand_block_break = view.settings().get('expand_block_break', global_settings.get('expand_block_break', '\n\n')) 31 | 32 | # instantiate formatter 33 | formatter = CssFormater(indentation, expand_block_break) 34 | 35 | selection = view.sel()[0] 36 | if detectSel and len(selection) > 0: 37 | self.format_selection(formatter, action, edit) 38 | else: 39 | self.format_whole_file(formatter, action, edit) 40 | 41 | def format_selection(self, formatter, action, edit): 42 | view = self.view 43 | regions = [] 44 | 45 | for sel in view.sel(): 46 | region = sublime.Region( 47 | view.line(min(sel.a, sel.b)).a, # line start of first line 48 | view.line(max(sel.a, sel.b)).b # line end of last line 49 | ) 50 | code = view.substr(region) 51 | code = formatter.run(code, action) 52 | #view.sel().clear() 53 | view.replace(edit, region, code) 54 | 55 | def format_whole_file(self, formatter, action, edit): 56 | view = self.view 57 | region = sublime.Region(0, view.size()) 58 | code = view.substr(region) 59 | code = formatter.run(code, action) + '\n' 60 | view.replace(edit, region, code) 61 | 62 | def is_visible(self): 63 | view = self.view 64 | file_name = view.file_name() 65 | syntax_path = view.settings().get('syntax') 66 | suffix_array = ['css', 'sass', 'scss', 'less', 'html', 'htm'] 67 | suffix = '' 68 | syntax = '' 69 | 70 | if file_name != None: # file exists, pull syntax type from extension 71 | suffix = os.path.splitext(file_name)[1][1:] 72 | if syntax_path != None: 73 | syntax = os.path.splitext(syntax_path)[0].split('/')[-1].lower() 74 | return suffix in suffix_array or syntax in suffix_array 75 | 76 | 77 | class FormatOnSave(sublime_plugin.EventListener): 78 | 79 | def on_pre_save(self, view): 80 | global_settings = sublime.load_settings('CSS Format.sublime-settings') 81 | 82 | should_format = view.settings().get('format_on_save', global_settings.get('format_on_save', False)) 83 | if not should_format: 84 | return 85 | 86 | file_filter = view.settings().get('format_on_save_filter', global_settings.get('format_on_save_filter', '\.(css|sass|scss|less)$')) 87 | if not re.search(file_filter, view.file_name()): 88 | return 89 | 90 | format_action = view.settings().get('format_on_save_action', global_settings.get('format_on_save_action', 'expand')) 91 | if not format_action: 92 | return 93 | 94 | view.run_command('css_format', {'action': format_action, 'detectSel': False}) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CSS Formatter for Sublime Text 2 | ============================== 3 | 4 | 5 | Description 6 | ----------- 7 | 8 | CSS Format is a CSS formatting plugin for Sublime Text, you can convert CSS/SASS/SCSS/LESS code to Expanded, Compact or Compressed format. CSS Format is just only a formatter, do not supports grammar check and auto correct feature. 9 | 10 | **Example:** 11 | 12 | * Expanded: 13 | 14 | ```css 15 | body { 16 | background: #fff; 17 | font: 12px/2em Arial, Helvetica, sans-serif; 18 | } 19 | ol, ul, li { 20 | margin: 0; 21 | padding: 0; 22 | } 23 | a { 24 | color: rgba(65, 131, 196, 0.8); 25 | } 26 | ``` 27 | 28 | * Expanded (Break Selectors): 29 | 30 | ```css 31 | body { 32 | background: #fff; 33 | font: 12px/2em Arial, Helvetica, sans-serif; 34 | } 35 | 36 | ol, 37 | ul, 38 | li { 39 | margin: 0; 40 | padding: 0; 41 | } 42 | 43 | a { 44 | color: rgba(65, 131, 196, 0.8); 45 | } 46 | ``` 47 | 48 | * Compact: 49 | 50 | ```css 51 | body { background: #fff; font: 12px/2em Arial, Helvetica, sans-serif; } 52 | ol, ul, li { margin: 0; padding: 0; } 53 | a { color: rgba(65, 131, 196, 0.8); } 54 | ``` 55 | 56 | * Compact (No Spaces): 57 | 58 | ```css 59 | body{background:#fff;font:12px/2em Arial,Helvetica,sans-serif;} 60 | ol,ul,li{margin:0;padding:0;} 61 | a{color:rgba(65,131,196,0.8);} 62 | ``` 63 | 64 | * Compact (Break Selectors): 65 | 66 | ```css 67 | body { background: #fff; font: 12px/2em Arial, Helvetica, sans-serif; } 68 | ol, 69 | ul, 70 | li { margin: 0; padding: 0; } 71 | a { color: rgba(65, 131, 196, 0.8); } 72 | ``` 73 | 74 | * Compact (Break Selectors, No Spaces): 75 | 76 | ```css 77 | body{background:#fff;font:12px/2em Arial,Helvetica,sans-serif;} 78 | ol, 79 | ul, 80 | li{margin:0;padding:0;} 81 | a{color:rgba(65,131,196,0.8);} 82 | ``` 83 | 84 | * Compressed: 85 | 86 | ```css 87 | body{background:#fff;font:12px/2em Arial,Helvetica,sans-serif}ol,ul,li{margin:0;padding:0}a{color:rgba(65,131,196,0.8)} 88 | ``` 89 | 90 | 91 | Installation 92 | ------------ 93 | 94 | **OPTION 1 - with Package Control (recommended)** 95 | 96 | The easiest way to install this package is through Package Control. 97 | 98 | 1. Install [Package Control](https://sublime.wbond.net/installation), follow instructions on the website. 99 | 100 | 2. Open command panel: `Ctrl+Shift+P` (Linux/Windows) or `Cmd+Shift+P` (OS X) and select **Package Control: Install Package**. 101 | 102 | 3. When packages list appears, type `CSS Format` and select it. 103 | 104 | 105 | **OPTION 2 - with Git** 106 | 107 | Clone the repository in your Sublime Text "Packages" directory: 108 | 109 | ```shell 110 | git clone git://github.com/mutian/Sublime-CSS-Format.git "CSS Format" 111 | ``` 112 | 113 | You can find your "Packages" inside the following directories: 114 | 115 | * OS X: 116 | `~/Library/Application Support/Sublime Text 2/Packages/` 117 | 118 | * Windows: 119 | `%APPDATA%/Sublime Text 2/Packages/` 120 | 121 | * Linux: 122 | `~/.Sublime Text 2/Packages/` 123 | 124 | 125 | **OPTION 3 - without Git** 126 | 127 | Download the latest source zip from [Github](https://github.com/mutian/Sublime-CSS-Format) and extract it into a new folder named `CSS Format` in your Sublime Text "Packages" folder. 128 | 129 | 130 | Usage 131 | ----- 132 | 133 | Select the code, or place cursor in the document, and execute commands in one of the following ways: 134 | 135 | * Context Menu: **CSS Format**. 136 | 137 | * Edit Menu: **Edit > CSS Format**. 138 | 139 | * Command Panel: Open command panel: `Ctrl+Shift+P` (Linux/Windows) or `Cmd+Shift+P` (OS X) and select **Format CSS: XXX**. 140 | 141 | 142 | Shortcuts 143 | --------- 144 | 145 | By default, CSS Format provides no keyboard shortcuts to avoid conflicts, but you can read the included `Example.sublime-keymaps` file to get an idea how to set up your own. 146 | 147 | 148 | Configuration 149 | ------------- 150 | 151 | There are a number of configuration options available to customize the behavior on save. For the latest information on what options are available, select the menu item **Preferences > Package Settings > CSS Format > Settings - Default**. 152 | 153 | **DO NOT** edit the default settings. Your changes will be lost when CSS Format is updated. ALWAYS edit the user settings by selecting **Preferences > Package Settings > CSS Format > Settings - User**. 154 | 155 | * indentation: Format indentation, you can set it to `" "`. By default, this is set to `"\t"` 156 | 157 | * expand_block_break: Set the line breaks after each rules block under `Expanded` format. By default, this is set to `"\n\n"`. 158 | 159 | * format_on_save: Set to `true` to trigger format on save. By default, this is set to `false`. 160 | 161 | * format_on_save_action: Format action. You can refer to **Settings - Default**. By default, this is set to `"expand"`. 162 | 163 | * format_on_save_filter: CSS Format matches the name of the file being saved against this regular expression to determine if a build should be triggered. By default, the setting has a value of `"\\.(css|sass|scss|less)$"`. 164 | 165 | 166 | Author 167 | ------ 168 | 169 | Created by **Mutian** ([http://mutian.wang](http://mutian.wang/)). 170 | 171 | For more info, you can send email to me: mutian(a)me.com! 172 | 173 | 174 | Acknowledgements 175 | ---------------- 176 | 177 | For Chinese information, please visit [http://mutian.wang/1508](http://mutian.wang/1508). 178 | -------------------------------------------------------------------------------- /libs/cssformatter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # Convert CSS/SASS/SCSS/LESS code to Expanded, Compact or Compressed format. 5 | # 6 | # Usage: 7 | # formatter = CssFormater() 8 | # formatter.run(code, action) 9 | # 10 | # Author: Mutian Wang 11 | # 12 | 13 | import re 14 | 15 | 16 | class CssFormater(): 17 | 18 | def __init__(self, indentation='\t', expand_block_break='\n\n'): 19 | self.indentation = indentation 20 | self.expand_block_break = expand_block_break 21 | 22 | 23 | def run(self, code, action='compact'): 24 | actFuns = { 25 | 'expand' : self.expand_rules, 26 | 'expand-bs' : self.expand_rules, # expand (break selectors) 27 | 'compact' : self.compact_rules, 28 | 'compact-bs' : self.compact_rules, # compact (break selectors) 29 | 'compact-ns' : self.compact_ns_rules, # compact (no spaces) 30 | 'compact-bs-ns' : self.compact_ns_rules, # compact (break selectors, no spaces) 31 | 'compress' : self.compress_rules 32 | } 33 | 34 | if action not in actFuns: 35 | return code 36 | 37 | # Comments 38 | if action == 'compress': 39 | # Remove comments 40 | code = re.sub(r'\s*\/\*[\s\S]*?\*\/\s*', '', code) 41 | else: 42 | # Protect comments 43 | commentReg = r'[ \t]*\/\*[\s\S]*?\*\/' 44 | comments = re.findall(commentReg, code) 45 | code = re.sub(commentReg, '!comment!', code) 46 | 47 | # Protect strings 48 | stringReg = r'(content\s*:|[\w-]+\s*=)\s*(([\'\"]).*?\3)\s*' 49 | strings = re.findall(stringReg, code) 50 | code = re.sub(stringReg, r'\1!string!', code) 51 | 52 | # Protect urls 53 | urlReg = r'((?:url|url-prefix|regexp)\([^\)]+\))' 54 | urls = re.findall(urlReg, code) 55 | code = re.sub(urlReg, '!url!', code) 56 | 57 | # Pre process 58 | code = re.sub(r'\s*([\{\}:;,])\s*', r'\1', code) # remove \s before and after characters {}:;, 59 | code = re.sub(r'([\[\(])\s*', r'\1', code) # remove space inner [ or ( 60 | code = re.sub(r'\s*([\)\]])', r'\1', code) # remove space inner ) or ] 61 | # code = re.sub(r'(\S+)\s*([\+>~])\s*(\S+)', r'\1\2\3', code) # remove \s before and after relationship selectors 62 | code = re.sub(r',[\d\s\.\#\+>~:]*\{', '{', code) # remove invalid selectors without \w 63 | code = re.sub(r'([;,])\1+', r'\1', code) # remove repeated ;, 64 | 65 | if action != 'compress': 66 | # Group selector 67 | if re.search('-bs', action): 68 | code = self.break_selectors(code) # break after selectors' , 69 | else: 70 | code = re.sub(r',\s*', ', ', code) # add space after , 71 | 72 | # Add space 73 | if re.search('-ns', action): 74 | code = re.sub(r', +', ',', code) # remove space after , 75 | code = re.sub(r'\s+!important', '!important', code) # remove space before !important 76 | else: 77 | code = re.sub(r'([A-Za-z-](?:\+_?)?):([^;\{]+[;\}])', r'\1: \2', code) # add space after properties' : 78 | code = re.sub(r'\s*!important', ' !important', code) # add space before !important 79 | 80 | # Process action rules 81 | code = actFuns[action](code) 82 | 83 | 84 | if action == 'compress': 85 | # Remove last semicolon 86 | code = code.replace(';}', '}') 87 | else: 88 | # Add blank line between each block in `expand-bs` mode 89 | if action == 'expand-bs': 90 | code = re.sub(r'\}\s*', '}\n\n', code) # double \n after } 91 | 92 | # Fix comments 93 | code = re.sub(r'\s*!comment!\s*@', '\n\n!comment!\n@', code) 94 | code = re.sub(r'\s*!comment!\s*([^\/\{\};]+?)\{', r'\n\n!comment!\n\1{', code) 95 | code = re.sub(r'\s*\n!comment!', '\n\n!comment!', code) 96 | 97 | # Backfill comments 98 | for i in range(len(comments)): 99 | code = re.sub(r'[ \t]*!comment!', comments[i], code, 1) 100 | 101 | # Indent 102 | code = self.indent_code(code) 103 | 104 | # Backfill strings 105 | for i in range(len(strings)): 106 | code = code.replace('!string!', strings[i][1], 1) 107 | 108 | # Backfill urls 109 | for i in range(len(urls)): 110 | code = code.replace('!url!', urls[i], 1) 111 | 112 | # Trim 113 | code = re.sub(r'^\s*(\S+(\s+\S+)*)\s*$', r'\1', code) 114 | 115 | return code 116 | 117 | 118 | # Expand Rules 119 | def expand_rules(self, code): 120 | code = re.sub('{', ' {\n', code) # add space before { and add \n after { 121 | 122 | code = re.sub(';', ';\n', code) # add \n after ; 123 | code = re.sub(r';\s*([^\{\};]+?)\{', r';\n\n\1{', code) # double \n between ; and include selector 124 | 125 | code = re.sub(r'\s*(!comment!)\s*;\s*', r' \1 ;\n', code) # fix comment before ; 126 | code = re.sub(r'(:[^:;]+;)\s*(!comment!)\s*', r'\1 \2\n', code) # fix comment after ; 127 | 128 | code = re.sub(r'\s*\}', '\n}', code) # add \n before } 129 | code = re.sub(r'\}\s*', '}' + self.expand_block_break, code) # add block break after } 130 | 131 | return code 132 | 133 | 134 | # Compact Rules 135 | def compact_rules(self, code): 136 | code = re.sub('{', ' { ', code) # add space before and after { 137 | code = re.sub(r'(@[\w-]*(document|font-feature-values|keyframes|media|supports)[^;]*?\{)\s*', r'\1\n', code) 138 | # add \n after @xxx { 139 | 140 | code = re.sub(';', '; ', code) # add space after ; 141 | code = re.sub(r'(@(charset|import|namespace).+?;)\s*', r'\1\n', code) # add \n after @charset & @import 142 | code = re.sub(r';\s*([^\};]+?\{)', r';\n\1', code) # add \n before included selector 143 | 144 | code = re.sub(r'\s*(!comment!)\s*;', r' \1 ;', code) # fix comment before ; 145 | code = re.sub(r'(:[^:;]+;)\s*(!comment!)\s*', r'\1 \2 ', code) # fix comment after ; 146 | 147 | code = re.sub(r'\s*\}', ' }', code) # add space before } 148 | code = re.sub(r'\}\s*', '}\n', code) # add \n after } 149 | 150 | return code 151 | 152 | 153 | # Compact Rules (no space) 154 | def compact_ns_rules(self, code): 155 | code = re.sub(r'(@[\w-]*(document|font-feature-values|keyframes|media|supports)[^;]*?\{)\s*', r'\1\n', code) 156 | # add \n after @xxx { 157 | 158 | code = re.sub(r'(@(charset|import|namespace).+?;)\s*', r'\1\n', code) # add \n after @charset & @import 159 | code = re.sub(r';\s*([^\};]+?\{)', r';\n\1', code) # add \n before included selector 160 | 161 | code = re.sub(r'\s*(!comment!)\s*;', r'\1;', code) # fix comment before ; 162 | code = re.sub(r'(:[^:;]+;)\s*(!comment!)\s*', r'\1\2', code) # fix comment after ; 163 | 164 | code = re.sub(r'\}\s*', '}\n', code) # add \n after } 165 | 166 | return code 167 | 168 | 169 | # Compress Rules 170 | def compress_rules(self, code): 171 | code = re.sub(r'\s*([\{\}:;,])\s*', r'\1', code) # remove \s before and after characters {}:;, again 172 | code = re.sub(r'\s+!important', '!important', code) # remove space before !important 173 | code = re.sub(r'((?:@charset|@import)[^;]+;)\s*', r'\1\n', code) # add \n after @charset & @import 174 | 175 | return code 176 | 177 | 178 | # Break after Selector 179 | def break_selectors(self, code): 180 | block = code.split('}') 181 | for i in range(len(block)): 182 | 183 | b = block[i].split('{') 184 | bLen = len(b) 185 | for j in range(bLen): 186 | 187 | if j == bLen - 1: 188 | b[j] = re.sub(r',\s*', ', ', b[j]) # add space after properties' , 189 | else: 190 | s = b[j].split(';') 191 | sLen = len(s) 192 | sLast = s[sLen - 1] 193 | 194 | for k in range(sLen - 1): 195 | s[k] = re.sub(r',\s*', ', ', s[k]) # add space after properties' , 196 | 197 | # For @document, @media 198 | if re.search(r'\s*@(document|media)', sLast): 199 | s[sLen - 1] = re.sub(r',\s*', ', ', sLast) # add space after @media's , 200 | 201 | # For mixins 202 | elif re.search(r'(\(|\))', sLast): 203 | u = sLast.split(')') 204 | for m in range(len(u)): 205 | v = u[m].split('(') 206 | vLen = len(v) 207 | if vLen < 2: 208 | continue 209 | v[0] = re.sub(r',\s*', ',\n', v[0]) 210 | v[1] = re.sub(r',\s*', ', ', v[1]) # do not break arguments 211 | u[m] = '('.join(v) 212 | s[sLen - 1] = ')'.join(u) 213 | 214 | # For selectors 215 | else: 216 | s[sLen - 1] = re.sub(r',\s*', ',\n', sLast) # add \n after selectors' , 217 | 218 | b[j] = ';'.join(s) 219 | 220 | block[i] = '{'.join(b) 221 | 222 | code = '}'.join(block) 223 | 224 | return code 225 | 226 | 227 | # Code Indent 228 | def indent_code(self, code): 229 | lines = code.split('\n') 230 | level = 0 231 | inComment = False 232 | outPrefix = '' 233 | 234 | for i in range(len(lines)): 235 | if not inComment: 236 | # Quote level adjustment 237 | validCode = re.sub(r'\/\*[\s\S]*?\*\/', '', lines[i]) 238 | validCode = re.sub(r'\/\*[\s\S]*', '', validCode) 239 | adjustment = validCode.count('{') - validCode.count('}') 240 | 241 | # Trim 242 | m = re.match(r'^(\s+)\/\*.*', lines[i]) 243 | if m is not None: 244 | outPrefix = m.group(1) 245 | lines[i] = re.sub(r'^' + outPrefix + '(.*)\s*$', r'\1', lines[i]) 246 | else: 247 | lines[i] = re.sub(r'^\s*(.*)\s*$', r'\1', lines[i]) 248 | else: 249 | # Quote level adjustment 250 | adjustment = 0 251 | 252 | # Trim 253 | lines[i] = re.sub(r'^' + outPrefix + '(.*)\s*$', r'\1', lines[i]) 254 | 255 | # Is next line in comment? 256 | commentQuotes = re.findall(r'\/\*|\*\/', lines[i]) 257 | for quote in commentQuotes: 258 | if inComment and quote == '*/': 259 | inComment = False 260 | elif quote == '/*': 261 | inComment = True 262 | 263 | # Quote level adjustment 264 | nextLevel = level + adjustment 265 | thisLevel = level if adjustment > 0 else nextLevel 266 | level = nextLevel 267 | 268 | # Add indentation 269 | lines[i] = self.indentation * thisLevel + lines[i] if lines[i] != '' else '' 270 | 271 | code = '\n'.join(lines) 272 | 273 | return code 274 | --------------------------------------------------------------------------------