Skip to content

Instantly share code, notes, and snippets.

@natchiketa
Created July 16, 2014 15:35
Show Gist options
  • Save natchiketa/30a03354eeb6b09de375 to your computer and use it in GitHub Desktop.
Save natchiketa/30a03354eeb6b09de375 to your computer and use it in GitHub Desktop.
Script for formatting CSS
#!/usr/bin/env python
#
# Convert CSS/SASS/SCSS/LESS code to Expanded, Compact or Compressed format.
# written by Mutian Wang <[email protected]>
#
# usage:
# format_code(code, action)
#
"""Convert CSS/SASS/SCSS/LESS code to Expanded, Compact or Compressed format."""
import re
import sys
def format_code(code, action='compact'):
actFuns = {
'expand' : expand_rules,
'expand-bs' : expand_rules, # expand (break selectors)
'compact' : compact_rules,
'compact-bs' : compact_rules, # compact (break selectors)
'compact-ns' : compact_ns_rules, # compact (no spaces)
'compact-bs-ns' : compact_ns_rules, # compact (break selectors, no spaces)
'compress' : compress_rules
}
# Protect Urls
urls = re.findall(r'url\([^\)]+\)', code)
code = re.sub(r'url\([^\)]+\)', 'url(~)', code)
# Pre Process
code = re.sub(r'\s*([\{\}:;,])\s*', r'\1', code) # remove \s before and after characters {}:;,
code = re.sub(r',[\d\s\.\#\+>:]*\{', '{', code) # remove invalid selector
code = re.sub(r';\s*;', ';', code) # remove superfluous ;
if action != 'compress':
# comment
code = re.sub(r'\/\*\s*([\s\S]+?)\s*\*\/', r'/* \1 */', code) # add space before and after comment content
code = re.sub(r'\}\s*(\/\*[\s\S]+?\*\/)\s*', r'}\n\1\n', code) # add \n before and after outside comment
# selectors group
if re.search(r'-bs', action):
code = break_selectors(code) # break after selectors' ,
else:
code = re.sub(r',(\S)', r', \1', code) # add space after ,
# add space after :
if re.search(r'-ns', action):
code = re.sub(r', +', ',', code) # remove space after ,
code = re.sub(r'\s+!important', '!important', code) # remove space before !important
else:
code = re.sub(r'([A-Za-z-]):([^;\{]+[;\}])', r'\1: \2', code) # add space after properties' :
code = re.sub(r'(http[s]?:) \/\/', r'\1//', code) # fix space after http[s]:
code = re.sub(r'\s*!important', ' !important', code) # add space before !important
# Process Action Rules
code = actFuns[action](code)
# Trim
code = re.sub(r'^\s*(\S+(\s+\S+)*)\s*$', r'\1', code)
# Indent
if action != 'compress':
code = indent_code(code)
else:
code = remove_last_semicolon(code)
# Backfill Urls
while re.search(r'url\(~\)', code):
code = re.sub(r'url\(~\)', urls[0], code, 1)
del urls[0]
return code
# Expand Rules
def expand_rules(code):
code = re.sub(r'(\S)\{(\S)', r'\1 {\n\2', code) # add space before { , and add \n after {
code = re.sub(r'(\S);([^\}])', r'\1;\n\2', code) # add \n after ;
# code = re.sub(r'(url\([^\)]*data:[\w\/-:]+)\;\s*', r'\1; ', code) # fix space after ; in data url
# code = re.sub(r'(url\([^\)]*charset=[\w-]+)\;\s*', r'\1; ', code) # fix space after ; in data url
code = re.sub(r'\;\s*(\/\*[^\n]*\*\/)\s*', r'; \1\n', code) # fix comment after ;
code = re.sub(r'((?:@charset|@import)[^;]+;)\s*', r'\1\n', code) # add \n after @charset & @import
code = re.sub(r'([^\}])\s*\}', r'\1\n}', code) # add \n before }
code = re.sub(r'\}', r'}\n', code) # add \n after }
return code
# Compact Rules
def compact_rules(code):
code = re.sub(r'(\S)\{(\S)', r'\1 { \2', code) # add space and after {
code = re.sub(r'((@media|@[\w-]*keyframes)[^\{]+\{)\s*', r'\1\n', code) # add \n after @media {
code = re.sub(r'(\S);([^\}])', r'\1; \2', code) # add space after ;
code = re.sub(r'\;\s*(\/\*[^\n]*\*\/)\s*', r'; \1\n', code) # fix comment after ;
code = re.sub(r'((?:@charset|@import)[^;]+;)\s*', r'\1\n', code) # add \n after @charset & @import
code = re.sub(r';\s*([^\};]+?\{)', r';\n\1', code) # add \n before included selector
code = re.sub(r'(\/\*[^\n]*\*\/)\s+\}', r'\1}', code) # remove \n between comment and }
code = re.sub(r'(\S)\}', r'\1 }', code) # add space before }
code = re.sub(r'\}\s*', r'}\n', code) # add \n after }
return code
# Compact Rules (no space)
def compact_ns_rules(code):
code = re.sub(r'((@media|@[\w-]*keyframes)[^\{]+\{)\s*', r'\1\n', code) # add \n after @media {
code = re.sub(r'\;\s*(\/\*[^\n]*\*\/)\s*', r'; \1\n', code) # fix comment after ;
code = re.sub(r'((?:@charset|@import)[^;]+;)\s*', r'\1\n', code) # add \n after @charset & @import
code = re.sub(r';\s*([^\};]+?\{)', r';\n\1', code) # add \n before included selector
code = re.sub(r'(\/\*[^\n]*\*\/)\s+\}', r'\1}', code) # remove \n between comment and }
code = re.sub(r'\}\s*', r'}\n', code) # add \n after }
return code
# Compress Rules
def compress_rules(code):
code = re.sub(r'\/\*[\s\S]+?\*\/', '', code) # remove non-empty comments, /**/ maybe a hack
code = re.sub(r'\s*([\{\}:;,])\s*', r'\1', code) # remove \s before and after characters {}:;, again
code = re.sub(r'\s+!important', '!important', code) # remove space before !important
code = re.sub(r'((?:@charset|@import)[^;]+;)\s*', r'\1\n', code) # add \n after @charset & @import
return code
# Break after Selector
def break_selectors(code):
block = code.split('}')
for i in range(len(block)):
b = block[i].split('{')
for j in range(len(b)):
if b[j].count('@import'):
s = b[j].split(';')
for k in range(len(s)):
if not s[k].count('@import'): # ignore @import
s[k] = re.sub(r',(\S)', r',\n\1', s[k])
b[j] = ';'.join(s)
else:
if j == len(b) - 1 or b[j].count('@media'):
b[j] = re.sub(r',(\S)', r', \1', b[j]) # add space after properties' or @media's ,
else:
b[j] = re.sub(r',(\S)', r',\n\1', b[j]) # add \n after selectors' ,
block[i] = '{'.join(b)
code = '}'.join(block)
return code
# Code Indent
def indent_code(code):
lines = code.split('\n')
level = 0
for i in range(len(lines)):
increment = lines[i].count('{') - lines[i].count('}')
level = level + increment
thisLevel = level - increment if increment > 0 else level
lines[i] = re.sub(r'\s*(\S+(\s+\S+)*)\s*', r'\1', lines[i]) # trim
lines[i] = '\t' * thisLevel + lines[i]
code = '\n'.join(lines)
return code
# Remove Last Semicolon
def remove_last_semicolon(code):
block = code.split('}')
for i in range(len(block)):
block[i] = re.sub(r';$', '', block[i])
code = '}'.join(block)
return code
def main():
data = sys.stdin.read()
code = format_code(data, 'expand')
sys.stdout.write(code)
if __name__ == '__main__':
main()
@natchiketa
Copy link
Author

Just threw this together in a couple of minutes so that I can use this in in Vim. From the Sublime Text plugin by Mutian Wang.

I use it like this:

Not my CSS :P

I put the script in a directory included in my PATH, giving it execute perms (chmod +x cssformat.py). As far as I know, passing the visual selection to an external command is a native Vim thing. No selection should do it to the whole buffer.

The format option is hard-coded—sorry! Obviously this should check sys.argv so you can use all the options.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment