diff --git a/Makefile b/Makefile index daac0fc12..cdef8ec52 100644 --- a/Makefile +++ b/Makefile @@ -81,16 +81,18 @@ js/lib/*.js: $(BUILD_DIR)/node $(BUILD_DIR)/generate $(wildcard js/src/*) $(wild # python package generation python/dist/*: $(BUILD_DIR)/python $(wildcard python/**/*.py) python/jsbeautifier/* python/cssbeautifier/* @echo Building python package... - rm -f python/dist/* + @rm -f python/dist/* @cd python && \ - cp setup-js.py setup.py && \ + cp setup-css.py setup.py && \ $(PYTHON) setup.py sdist && \ rm setup.py @cd python && \ - cp setup-css.py setup.py && \ + cp setup-js.py setup.py && \ $(PYTHON) setup.py sdist && \ rm setup.py - $(SCRIPT_DIR)/python-rel pip install -U python/dist/* + # Order matters here! Install css then js to make sure the local dist version of js is used + $(SCRIPT_DIR)/python-rel pip install -U python/dist/cssbeautifier* + $(SCRIPT_DIR)/python-rel pip install -U python/dist/jsbeautifier* # python package generation build/*.tgz: js/lib/*.js @@ -129,6 +131,9 @@ $(BUILD_DIR)/node: package.json package-lock.json | $(BUILD_DIR) $(BUILD_DIR)/python: python/setup-js.py python/setup-css.py | $(BUILD_DIR) $(BUILD_DIR)/virtualenv @$(PYTHON) --version + # Order matters here! Install css then js to make sure the local dist version of js is used + @cp ./python/setup-css.py ./python/setup.py + $(SCRIPT_DIR)/python-dev pip install -e ./python @cp ./python/setup-js.py ./python/setup.py $(SCRIPT_DIR)/python-dev pip install -e ./python @rm ./python/setup.py diff --git a/python/cssbeautifier/_main.py b/python/cssbeautifier/_main.py index e1f4e31e6..655149ecf 100644 --- a/python/cssbeautifier/_main.py +++ b/python/cssbeautifier/_main.py @@ -31,7 +31,7 @@ import copy import getopt from cssbeautifier.__version__ import __version__ -from jsbeautifier import isFileDifferent, mkdir_p +from jsbeautifier.cli import * from cssbeautifier.css.options import BeautifierOptions from cssbeautifier.css.beautifier import Beautifier @@ -48,22 +48,7 @@ def beautify(string, opts=None): def beautify_file(file_name, opts=None): - if file_name == "-": # stdin - try: - if sys.stdin.isatty(): - raise Exception() - - stream = sys.stdin - except Exception: - print("Must pipe input or define input file.\n", file=sys.stderr) - usage(sys.stderr) - raise Exception() - else: - stream = open(file_name) - - content = "".join(stream.readlines()) - b = Beautifier(content, opts) - return b.beautify() + return process_file(file_name, opts, beautify) def usage(stream=sys.stdout): @@ -125,6 +110,7 @@ def main(): argv, "hvio:rs:c:e:tnb:", [ + "editorconfig", "help", "usage", "version", @@ -150,11 +136,11 @@ def main(): css_options = default_options() - file = None - outfile = "stdout" + filepath_params = [] + filepath_params.extend(args) + + outfile_param = "stdout" replace = False - if len(args) == 1: - file = args[0] for opt, arg in opts: if opt in ("--stdin", "-i"): @@ -190,41 +176,34 @@ def main(): css_options.space_around_combinator = True elif opt in ("--indent-empty-lines"): css_options.indent_empty_lines = True - - if not file: - file = "-" + elif opt in ("--editorconfig"): + css_options.editorconfig = True try: - if outfile == "stdout" and replace and not file == "-": - outfile = file - - pretty = beautify_file(file, css_options) - - if outfile == "stdout": - # python automatically converts newlines in text to "\r\n" when on windows - # switch to binary to prevent this - if sys.platform == "win32": - import msvcrt - - msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - - sys.stdout.write(pretty) - else: - if isFileDifferent(outfile, pretty): - mkdir_p(os.path.dirname(outfile)) - - # python automatically converts newlines in text to "\r\n" when on windows - # set newline to empty to prevent this - with io.open(outfile, "wt", newline="") as f: - print("writing " + outfile, file=sys.stderr) - try: - f.write(pretty) - except TypeError: - # This is not pretty, but given how we did the version import - # it is the only way to do this without having setup.py - # fail on a missing six dependency. - six = __import__("six") - f.write(six.u(pretty)) + filepaths, replace = get_filepaths_from_params(filepath_params, replace) + for filepath in filepaths: + if not replace: + outfile = outfile_param + else: + outfile = filepath + + css_options = integrate_editorconfig_options( + filepath, css_options, outfile, "js" + ) + + pretty = beautify_file(filepath, css_options) + + write_beautified_output(pretty, css_options, outfile) + + except MissingInputStreamError: + print("Must pipe input or define at least one file.\n", file=sys.stderr) + usage(sys.stderr) + return 1 + + except UnicodeError as ex: + print("Error while decoding input or encoding output:", file=sys.stderr) + print(ex, file=sys.stderr) + return 1 except Exception as ex: print(ex, file=sys.stderr) diff --git a/python/jsbeautifier/__init__.py b/python/jsbeautifier/__init__.py index fa5e0889b..56babdb0d 100644 --- a/python/jsbeautifier/__init__.py +++ b/python/jsbeautifier/__init__.py @@ -10,6 +10,7 @@ import copy import glob from jsbeautifier.__version__ import __version__ +from jsbeautifier.cli import * from jsbeautifier.javascript.options import BeautifierOptions from jsbeautifier.javascript.beautifier import Beautifier @@ -63,9 +64,13 @@ # # Here are the available options: (read source) - -class MissingInputStreamError(Exception): - pass +__all__ = [ + "default_options", + "beautify", + "beautify_file", + "usage", + "main", +] def default_options(): @@ -77,75 +82,8 @@ def beautify(string, opts=default_options()): return b.beautify(string, opts) -def set_file_editorconfig_opts(filename, js_options): - from editorconfig import get_properties, EditorConfigError - - try: - _ecoptions = get_properties(os.path.abspath(filename)) - - if _ecoptions.get("indent_style") == "tab": - js_options.indent_with_tabs = True - elif _ecoptions.get("indent_style") == "space": - js_options.indent_with_tabs = False - - if _ecoptions.get("indent_size"): - js_options.indent_size = int(_ecoptions["indent_size"]) - - if _ecoptions.get("max_line_length"): - if _ecoptions.get("max_line_length") == "off": - js_options.wrap_line_length = 0 - else: - js_options.wrap_line_length = int(_ecoptions["max_line_length"]) - - if _ecoptions.get("insert_final_newline") == "true": - js_options.end_with_newline = True - elif _ecoptions.get("insert_final_newline") == "false": - js_options.end_with_newline = False - - if _ecoptions.get("end_of_line"): - if _ecoptions["end_of_line"] == "cr": - js_options.eol = "\r" - elif _ecoptions["end_of_line"] == "lf": - js_options.eol = "\n" - elif _ecoptions["end_of_line"] == "crlf": - js_options.eol = "\r\n" - - except EditorConfigError: - # do not error on bad editor config - print("Error loading EditorConfig. Ignoring.", file=sys.stderr) - - def beautify_file(file_name, opts=default_options()): - input_string = "" - if file_name == "-": # stdin - if sys.stdin.isatty(): - raise MissingInputStreamError() - - stream = sys.stdin - if platform.platform().lower().startswith("windows"): - if sys.version_info.major >= 3: - # for python 3 on windows this prevents conversion - stream = io.TextIOWrapper(sys.stdin.buffer, newline="") - elif platform.architecture()[0] == "32bit": - # for python 2 x86 on windows this prevents conversion - import msvcrt - - msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) - else: - raise Exception( - "Pipe to stdin not supported on Windows with Python 2.x 64-bit." - ) - - input_string = stream.read() - - # if you pipe an empty string, that is a failure - if input_string == "": - raise MissingInputStreamError() - else: - stream = io.open(file_name, "rt", newline="", encoding="UTF-8") - input_string = stream.read() - - return beautify(input_string, opts) + return process_file(file_name, opts, beautify) def usage(stream=sys.stdout): @@ -217,24 +155,6 @@ def usage(stream=sys.stdout): return 0 -def mkdir_p(path): - try: - if path: - os.makedirs(path) - except OSError as exc: # Python >2.5 - if exc.errno == errno.EEXIST and os.path.isdir(path): - pass - else: - raise Exception() - - -def isFileDifferent(filepath, expected): - try: - return "".join(io.open(filepath, "rt", newline="").readlines()) != expected - except BaseException: - return True - - def main(): argv = sys.argv[1:] @@ -357,108 +277,20 @@ def main(): return usage() try: - filepaths = [] - if not filepath_params or ( - len(filepath_params) == 1 and filepath_params[0] == "-" - ): - # default to stdin - filepath_params = [] - filepaths.append("-") - - for filepath_param in filepath_params: - # ignore stdin setting if files are specified - if "-" == filepath_param: - continue - - # Check if each literal filepath exists - if os.path.isfile(filepath_param): - filepaths.append(filepath_param) - elif "*" in filepath_param or "?" in filepath_param: - # handle globs - # empty result is okay - if sys.version_info.major == 2 or ( - sys.version_info.major == 3 and sys.version_info.minor <= 4 - ): - if "**" in filepath_param: - raise Exception( - "Recursive globs not supported on Python <= 3.4." - ) - filepaths.extend(glob.glob(filepath_param)) - else: - filepaths.extend(glob.glob(filepath_param, recursive=True)) - else: - # not a glob and not a file - raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), filepath_param) - - if len(filepaths) > 1: - replace = True - elif filepaths and filepaths[0] == "-": - replace = False - - # remove duplicates - filepaths = set(filepaths) - + filepaths, replace = get_filepaths_from_params(filepath_params, replace) for filepath in filepaths: if not replace: outfile = outfile_param else: outfile = filepath - # Editorconfig used only on files, not stdin - if getattr(js_options, "editorconfig"): - editorconfig_filepath = filepath - - if editorconfig_filepath == "-": - if outfile != "stdout": - editorconfig_filepath = outfile - else: - fileType = "js" - editorconfig_filepath = "stdin." + fileType - - # debug("EditorConfig is enabled for ", editorconfig_filepath); - js_options = copy.copy(js_options) - set_file_editorconfig_opts(editorconfig_filepath, js_options) + js_options = integrate_editorconfig_options( + filepath, js_options, outfile, "js" + ) pretty = beautify_file(filepath, js_options) - if outfile == "stdout": - stream = sys.stdout - - # python automatically converts newlines in text to "\r\n" when on windows - # switch to binary to prevent this - if platform.platform().lower().startswith("windows"): - if sys.version_info.major >= 3: - # for python 3 on windows this prevents conversion - stream = io.TextIOWrapper(sys.stdout.buffer, newline="") - elif platform.architecture()[0] == "32bit": - # for python 2 x86 on windows this prevents conversion - import msvcrt - - msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - else: - raise Exception( - "Pipe to stdout not supported on Windows with Python 2.x 64-bit." - ) - - stream.write(pretty) - else: - if isFileDifferent(outfile, pretty): - mkdir_p(os.path.dirname(outfile)) - - # python automatically converts newlines in text to "\r\n" when on windows - # set newline to empty to prevent this - with io.open(outfile, "wt", newline="", encoding="UTF-8") as f: - print("beautified " + outfile, file=sys.stdout) - try: - f.write(pretty) - except TypeError: - # This is not pretty, but given how we did the version import - # it is the only way to do this without having setup.py - # fail on a missing six dependency. - six = __import__("six") - f.write(six.u(pretty)) - elif not js_options.keep_quiet: - print("beautified " + outfile + " - unchanged", file=sys.stdout) + write_beautified_output(pretty, js_options, outfile) except MissingInputStreamError: print("Must pipe input or define at least one file.\n", file=sys.stderr) diff --git a/python/jsbeautifier/cli/__init__.py b/python/jsbeautifier/cli/__init__.py new file mode 100644 index 000000000..c6e1a29ba --- /dev/null +++ b/python/jsbeautifier/cli/__init__.py @@ -0,0 +1,241 @@ +from __future__ import print_function +import sys +import os +import platform +import io +import getopt +import re +import string +import errno +import copy +import glob +from jsbeautifier.__version__ import __version__ +from jsbeautifier.javascript.options import BeautifierOptions +from jsbeautifier.javascript.beautifier import Beautifier + +# +# The MIT License (MIT) + +# Copyright (c) 2007-2020 Einar Lielmanis, Liam Newman, and contributors. + +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = [ + "MissingInputStreamError", + "process_file", + "get_filepaths_from_params", + "integrate_editorconfig_options", + "write_beautified_output", +] + + +class MissingInputStreamError(Exception): + pass + + +def set_file_editorconfig_opts(filename, js_options): + from editorconfig import get_properties, EditorConfigError + + try: + _ecoptions = get_properties(os.path.abspath(filename)) + + if _ecoptions.get("indent_style") == "tab": + js_options.indent_with_tabs = True + elif _ecoptions.get("indent_style") == "space": + js_options.indent_with_tabs = False + + if _ecoptions.get("indent_size"): + js_options.indent_size = int(_ecoptions["indent_size"]) + + if _ecoptions.get("max_line_length"): + if _ecoptions.get("max_line_length") == "off": + js_options.wrap_line_length = 0 + else: + js_options.wrap_line_length = int(_ecoptions["max_line_length"]) + + if _ecoptions.get("insert_final_newline") == "true": + js_options.end_with_newline = True + elif _ecoptions.get("insert_final_newline") == "false": + js_options.end_with_newline = False + + if _ecoptions.get("end_of_line"): + if _ecoptions["end_of_line"] == "cr": + js_options.eol = "\r" + elif _ecoptions["end_of_line"] == "lf": + js_options.eol = "\n" + elif _ecoptions["end_of_line"] == "crlf": + js_options.eol = "\r\n" + + except EditorConfigError: + # do not error on bad editor config + print("Error loading EditorConfig. Ignoring.", file=sys.stderr) + + +def process_file(file_name, opts, beautify_code): + input_string = "" + if file_name == "-": # stdin + if sys.stdin.isatty(): + raise MissingInputStreamError() + + stream = sys.stdin + if platform.platform().lower().startswith("windows"): + if sys.version_info.major >= 3: + # for python 3 on windows this prevents conversion + stream = io.TextIOWrapper(sys.stdin.buffer, newline="") + elif platform.architecture()[0] == "32bit": + # for python 2 x86 on windows this prevents conversion + import msvcrt + + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) + else: + raise Exception( + "Pipe to stdin not supported on Windows with Python 2.x 64-bit." + ) + + input_string = stream.read() + + # if you pipe an empty string, that is a failure + if input_string == "": + raise MissingInputStreamError() + else: + stream = io.open(file_name, "rt", newline="", encoding="UTF-8") + input_string = stream.read() + + return beautify_code(input_string, opts) + + +def mkdir_p(path): + try: + if path: + os.makedirs(path) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise Exception() + + +def isFileDifferent(filepath, expected): + try: + return "".join(io.open(filepath, "rt", newline="").readlines()) != expected + except BaseException: + return True + + +def get_filepaths_from_params(filepath_params, replace): + filepaths = [] + if not filepath_params or (len(filepath_params) == 1 and filepath_params[0] == "-"): + # default to stdin + filepath_params = [] + filepaths.append("-") + + for filepath_param in filepath_params: + # ignore stdin setting if files are specified + if "-" == filepath_param: + continue + + # Check if each literal filepath exists + if os.path.isfile(filepath_param): + filepaths.append(filepath_param) + elif "*" in filepath_param or "?" in filepath_param: + # handle globs + # empty result is okay + if sys.version_info.major == 2 or ( + sys.version_info.major == 3 and sys.version_info.minor <= 4 + ): + if "**" in filepath_param: + raise Exception("Recursive globs not supported on Python <= 3.4.") + filepaths.extend(glob.glob(filepath_param)) + else: + filepaths.extend(glob.glob(filepath_param, recursive=True)) + else: + # not a glob and not a file + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), filepath_param) + + if len(filepaths) > 1: + replace = True + elif filepaths and filepaths[0] == "-": + replace = False + + # remove duplicates + filepaths = set(filepaths) + + return filepaths, replace + + +def integrate_editorconfig_options(filepath, local_options, outfile, default_file_type): + # Editorconfig used only on files, not stdin + if getattr(local_options, "editorconfig"): + editorconfig_filepath = filepath + + if editorconfig_filepath == "-": + if outfile != "stdout": + editorconfig_filepath = outfile + else: + fileType = default_file_type + editorconfig_filepath = "stdin." + fileType + + # debug("EditorConfig is enabled for ", editorconfig_filepath); + local_options = copy.copy(local_options) + set_file_editorconfig_opts(editorconfig_filepath, local_options) + + return local_options + + +def write_beautified_output(pretty, local_options, outfile): + if outfile == "stdout": + stream = sys.stdout + + # python automatically converts newlines in text to "\r\n" when on windows + # switch to binary to prevent this + if platform.platform().lower().startswith("windows"): + if sys.version_info.major >= 3: + # for python 3 on windows this prevents conversion + stream = io.TextIOWrapper(sys.stdout.buffer, newline="") + elif platform.architecture()[0] == "32bit": + # for python 2 x86 on windows this prevents conversion + import msvcrt + + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + else: + raise Exception( + "Pipe to stdout not supported on Windows with Python 2.x 64-bit." + ) + + stream.write(pretty) + else: + if isFileDifferent(outfile, pretty): + mkdir_p(os.path.dirname(outfile)) + + # python automatically converts newlines in text to "\r\n" when on windows + # set newline to empty to prevent this + with io.open(outfile, "wt", newline="", encoding="UTF-8") as f: + print("beautified " + outfile, file=sys.stdout) + try: + f.write(pretty) + except TypeError: + # This is not pretty, but given how we did the version import + # it is the only way to do this without having setup.py + # fail on a missing six dependency. + six = __import__("six") + f.write(six.u(pretty)) + elif not local_options.keep_quiet: + print("beautified " + outfile + " - unchanged", file=sys.stdout) diff --git a/python/jsbeautifier/core/options.py b/python/jsbeautifier/core/options.py index 6a6dd8ae9..b38a6bc57 100644 --- a/python/jsbeautifier/core/options.py +++ b/python/jsbeautifier/core/options.py @@ -71,6 +71,9 @@ def __init__(self, options=None, merge_child_field=None): "wrap_line_length", self._get_number("max_char") ) + # Support editor config setting + self.editorconfig = False + self.indent_empty_lines = self._get_boolean("indent_empty_lines") # valid templating languages ['django', 'erb', 'handlebars', 'php'] diff --git a/python/jsbeautifier/javascript/options.py b/python/jsbeautifier/javascript/options.py index 15a45bf9f..541a6d079 100644 --- a/python/jsbeautifier/javascript/options.py +++ b/python/jsbeautifier/javascript/options.py @@ -93,7 +93,6 @@ def __init__(self, options=None): # For testing of beautify preserve:start directive self.test_output_raw = False - self.editorconfig = False # force opts.space_after_anon_function to true if opts.jslint_happy if self.jslint_happy: diff --git a/python/jsbeautifier/tests/shell-test.sh b/python/jsbeautifier/tests/shell-test.sh index 7e7d8c1d5..8928fc7eb 100755 --- a/python/jsbeautifier/tests/shell-test.sh +++ b/python/jsbeautifier/tests/shell-test.sh @@ -383,8 +383,10 @@ test_cli_js_beautify() } main() { - #test_cli_common css-beautify - #test_cli_common html-beautify + + test_cli_common js-beautify "$SCRIPT_DIR/../../../tools/python-dev css-beautify" + test_cli_common js-beautify "$SCRIPT_DIR/../../../tools/python-rel css-beautify" + test_cli_common js-beautify "$SCRIPT_DIR/../../../tools/python-dev js-beautify" test_cli_common js-beautify "$SCRIPT_DIR/../../../tools/python-rel js-beautify" diff --git a/python/setup-js.py b/python/setup-js.py index 171161bee..753d13ccb 100755 --- a/python/setup-js.py +++ b/python/setup-js.py @@ -45,6 +45,7 @@ def run_tests(self): "jsbeautifier.tests", "jsbeautifier.tests.generated", "jsbeautifier.core", + "jsbeautifier.cli", "jsbeautifier.javascript", "jsbeautifier.unpackers", "jsbeautifier.unpackers.tests",