def cleanup_stylesheets( file_path_normalized, windows_newline, fix_extra_newlines, property_order, rules_skip ): """ Cleans up stylesheets. Sorts the css properties in the file, by the specified property order. Ensures that all property lines are correctly terminated with semicolon. :type file_path_normalized: String :param file_path_normalized: The file path normalized. :type windows_newline: bool :param windows_newline: If the windows newline should be used. :type fix_extra_newlines: bool :param fix_extra_newlines: If the extra newlines should be fixed. :type property_order: List :param property_order: The list with the explicit order of property names. :type rules_skip: List :param rules_skip: The list of specific rules to skip. """ # opens the file for reading in text mode so that the # lines are read in normalized mode file = open(file_path_normalized, "rb") try: # applies the property cleaning, this should run the # complete set of rules and clean the file string_buffer = cleanup_properties( file, windows_newline = windows_newline, fix_extra_newlines = fix_extra_newlines, property_order = property_order, rules_skip = rules_skip ) # retrieves the string value from the output # buffer to be written to the file and then # encodes it using the default encoding of css string_value = string_buffer.getvalue() string_value = string_value.encode("utf-8") except Exception as exception: # retrieves the exception string and uses it in the log # message to be printed to the standard output, then logs # the complete traceback messages to the same output exception_string = legacy.UNICODE(exception) extra.error("%s. Skipping file %s" % (exception_string, file_path_normalized)) traceback.print_exc(file = sys.stdout) # skips writing to the file return finally: # closes the file for reading file.close() # opens the file for writing and then outputs the # final normalized stylesheet contents into it file = open(file_path_normalized, "wb") try: file.write(string_value) finally: file.close()
def cleanup_properties( input_buffer, windows_newline = True, fix_extra_newlines = True, property_order = (), rules_skip = () ): """ Cleans up the property lines. Sorts the css properties in the file, by the specified property order. Ensures that all property lines are correctly terminated with semicolon. :type input_buffer: StringBuffer :param input_buffer: The input buffer. :type windows_newline: bool :param windows_newline: If the windows newline should be used. :type windows_newline: bool :param windows_newline: If the extra newlines should be fixed. :type fix_extra_newlines: bool :param fix_extra_newlines: If the extra newlines should be fixed. :type property_order: List :param property_order: The list with the explicit order of property names. :type rules_skip: List :param rules_skip: The list of specific rules to skip. """ # reads the input lines and counts the number of lines # in the buffer setting it as the number of "original" lines lines = input_buffer.readlines() number_original_lines = len(lines) # creates a string buffer that will hold the output of the # cleanup operation over the current stylesheet file output_buffer = legacy.StringIO() # initializes a series of state variables that will control # the way the parser/generator will work through the file rule_started = False line_number = 0 open_rule_count = 0 newlines = 0 comments_started = 0 needs_newline = False # for each of the input lines it's going to run the iteration # loop and match it against the proper rules for line in lines: # decodes the current line as a processing using unicode # characters is required for correct results line = line.decode("utf-8") # increments the line number as one more line is going # to be processes through the current iteration line_number += 1 # updates the comparison key function get_comparison_key = lambda property_line: get_property_index( property_line, property_order, line_number ) # in case the line contains a single line comment, # does nothing, will write the line as is if "/*" in line and "*/" in line and not "*/*/" in line: pass # in case the line contains the end of multiline comment elif "*/" in line: # in case the comment mode is currently not enabled prints # a warning as we're trying to close a comment without opening if not comments_started: extra.error( "Found closing comment without corresponding opening at line %d" % line_number ) # in case there's more that one closing comment defined under # the same line prints an error as this is not allowed if line.count("*/") > 1: extra.error( "More that one closing comment at line %d" % line_number ) # decrements the comments started counter and then # enables the needs newline flag comments_started = 0 needs_newline = True # in case the line contains a the start of multiline comment elif "/*" in line: # in case the comment mode is already enabled (at least one) # prints a waning about the double opening if comments_started: extra.warn( "Found opening comment inside open comment at line %d" % line_number ) # in case there's more that one opening comment defined under # the same line prints an error as this is not allowed if line.count("/*") > 1: extra.error( "More that one opening comment at line %d" % line_number ) # increments the comments started counter comments_started += 1 # in case this is a comment line (comment started) # does nothing, will just write the line as is elif comments_started: pass # in case the line contains an at rule specification elif AT_RULES_REGEX.match(line): # after an at rule, a newline must follow needs_newline = True # in case the line contains a full rule specification # does nothing, will just write line as is as there's # very few thing possible to optimize under this situation elif "{" in line and "}" in line: line = line.strip() needs_newline = True rule_started = False # in case this is a rule start line elif "{" in line: # increments the open rule count open_rule_count += 1 # in case this is an actual rule, meaning that the open rule # count value is positive (assertion valid) if open_rule_count > 0: # resets the rule lines rule_lines = [] # signals the rule started flag, # in case the rule is no to be skipped rule_started = not skip_rule(line, rules_skip) # replaces the various css tokens with proper separators # meaning that no extra spaces between them are allowed line = TOKEN_REGEX.sub(r" \1 ", line) # runs the space replacement operation so that the # extra spaces in the line (not required) are removed # and the line is simplified line = SPACE_REGEX.sub(" ", line) line = line.strip() # calculates the space padding that is going to be pre-pended # to the line and then adds it to the line note that the open # rule count is decremented by one (opened on this line) padding = (open_rule_count - 1) * 4 * " " line = padding + line # flushes the current line value to the output buffer and then # continues the current loop (avoids general operations) write_line(output_buffer, line, windows_newline) continue elif "}" in line: # decrements the open rule count open_rule_count -= 1 # in case there is a mismatch in open and closed rules # must raise an exception indicating the problem if open_rule_count < 0: raise Exception("mismatched rules found") # strips the current line removing any extra (not expected) # values and simplifying the current line value padding = open_rule_count * 4 * " " line = line.strip() line = padding + line # in case the rule set does not contain any property # must log an information message about the empty rule if rule_started and not rule_lines: extra.warn("Empty stylesheet rule at line %d" % line_number) # updates the flag to signal the rule has ended rule_started = False # sorts the various property lines and the processes them property_lines = sorted(rule_lines, key = get_comparison_key) property_lines = process_property_lines( property_lines, line_number, indentation = open_rule_count + 1, avoid_empty = fix_extra_newlines ) property_lines = sorted(property_lines, key = get_comparison_key) # writes the lines to the buffer, considering the windows newline # and then writes the line write_lines( output_buffer, property_lines, windows_newline = windows_newline, avoid_empty = fix_extra_newlines ) write_line(output_buffer, line, windows_newline = windows_newline) # resets the newlines counter and then # enables the needs newline flag newlines = 0 needs_newline = True rule_lines = [] # skips further processing continue # in case this is part of rule selector declaration # the typical rule replacement operations are applied elif line.rstrip().endswith(","): line = TOKEN_REGEX.sub(r" \1 ", line) line = SPACE_REGEX.sub(" ", line) line = COMMA_REGEX.sub(r"\1", line) line = line.strip() # in case this line is part of a valid rule set elif rule_started: # appends the line to the rule set, and then # skips outputting the line to the buffer rule_lines.append(line) continue # in case the between rules mode is active elif not rule_started and not line.strip(): # increments the newlines count newlines += 1 # otherwise in case this is an extra newline, must either # remove it or print a warning message depending on mode if not needs_newline and newlines > 1: if fix_extra_newlines: continue else: extra.warn("Found extra newline at line %d" % line_number) # disables the needs newline flag needs_newline = False else: # warns about the statement outside a valid rule extra.warn("Found statement outside rule at line %d" % line_number) # "calculates" the padding string value taking into account the # current open rule count value and then removes any left space # compatible values and replaces them with proper indentation padding = open_rule_count * 4 * " " line = line.lstrip() line = padding + line # writes the line to the output buffer taking into # account the windows newline control flag write_line(output_buffer, line, windows_newline) # in case there is a mismatch in open and closed rules # must raise an exception indicating the problem if not open_rule_count == 0: raise Exception("mismatched rules found") # retrieves the output buffer value and then counts the # number of lines contained in it (assertion validation) output_buffer_value = output_buffer.getvalue() number_lines = output_buffer_value.count("\n") if not number_lines == number_original_lines and not fix_extra_newlines: raise Exception( "number of lines in processed file (%d) is different from original file (%d)" %\ (number_lines, number_original_lines) ) return output_buffer