# The MIT License (MIT)
#
# Copyright (c) 2007-2018 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.

import re
import string
import copy
from ..core.token import Token
from .tokenizer import Tokenizer
from .tokenizer import TOKEN
from .options import BeautifierOptions
from ..core.output import Output


def default_options():
    return BeautifierOptions()


class BeautifierFlags:
    def __init__(self, mode):
        self.mode = mode
        self.parent = None
        self.last_token = Token(TOKEN.START_BLOCK, "")
        self.last_word = ""
        self.declaration_statement = False
        self.declaration_assignment = False
        self.multiline_frame = False
        self.inline_frame = False
        self.if_block = False
        self.else_block = False
        self.class_start_block = False
        self.do_block = False
        self.do_while = False
        self.import_block = False
        self.in_case = False
        self.in_case_statement = False
        self.case_body = False
        self.case_block = False
        self.indentation_level = 0
        self.alignment = 0
        self.line_indent_level = 0
        self.start_line_index = 0
        self.ternary_depth = 0

    def apply_base(self, flags_base, added_newline):
        next_indent_level = flags_base.indentation_level
        if not added_newline and flags_base.line_indent_level > next_indent_level:
            next_indent_level = flags_base.line_indent_level

        self.parent = flags_base
        self.last_token = flags_base.last_token
        self.last_word = flags_base.last_word
        self.indentation_level = next_indent_level


OPERATOR_POSITION = {
    "before_newline": "before-newline",
    "after_newline": "after-newline",
    "preserve_newline": "preserve-newline",
}
OPERATOR_POSITION_BEFORE_OR_PRESERVE = [
    OPERATOR_POSITION["before_newline"],
    OPERATOR_POSITION["preserve_newline"],
]


class MODE:
    (
        BlockStatement,
        Statement,
        ObjectLiteral,
        ArrayLiteral,
        ForInitializer,
        Conditional,
        Expression,
    ) = range(7)


def remove_redundant_indentation(output, frame):
    # This implementation is effective but has some issues:
    #     - can cause line wrap to happen too soon due to indent removal
    #           after wrap points are calculated
    # These issues are minor compared to ugly indentation.

    if (
        frame.multiline_frame
        or frame.mode == MODE.ForInitializer
        or frame.mode == MODE.Conditional
    ):
        return

    # remove one indent from each line inside this section
    output.remove_indent(frame.start_line_index)


def reserved_word(token, word):
    return token and token.type == TOKEN.RESERVED and token.text == word


def reserved_array(token, words):
    return token and token.type == TOKEN.RESERVED and token.text in words


_special_word_set = frozenset(
    [
        "case",
        "return",
        "do",
        "if",
        "throw",
        "else",
        "await",
        "break",
        "continue",
        "async",
    ]
)


class Beautifier:
    def __init__(self, opts=None):
        import jsbeautifier.javascript.acorn as acorn

        self.acorn = acorn
        self._options = BeautifierOptions(opts)

        self._blank_state()

    def _blank_state(self, js_source_text=None):
        if js_source_text is None:
            js_source_text = ""

        # internal flags
        self._flags = None
        self._previous_flags = None
        self._flag_store = []
        self._tokens = None

        if self._options.eol == "auto":
            self._options.eol = "\n"
            if self.acorn.lineBreak.search(js_source_text or ""):
                self._options.eol = self.acorn.lineBreak.search(js_source_text).group()

        baseIndentString = re.search("^[\t ]*", js_source_text).group(0)
        self._last_last_text = ""  # pre-last token text

        self._output = Output(self._options, baseIndentString)
        # If testing the ignore directive, start with output disable set to
        # true
        self._output.raw = self._options.test_output_raw

        self.set_mode(MODE.BlockStatement)
        return js_source_text

    def beautify(self, source_text="", opts=None):
        if opts is not None:
            self._options = BeautifierOptions(opts)

        source_text = source_text or ""
        if self._options.disabled:
            return source_text

        source_text = self._blank_state(source_text)

        source_text = self.unpack(source_text, self._options.eval_code)

        self._tokens = Tokenizer(source_text, self._options).tokenize()

        for current_token in self._tokens:
            self.handle_token(current_token)

            self._last_last_text = self._flags.last_token.text
            self._flags.last_token = current_token

        sweet_code = self._output.get_code(self._options.eol)

        return sweet_code

    def handle_token(self, current_token, preserve_statement_flags=False):
        if current_token.type == TOKEN.START_EXPR:
            self.handle_start_expr(current_token)
        elif current_token.type == TOKEN.END_EXPR:
            self.handle_end_expr(current_token)
        elif current_token.type == TOKEN.START_BLOCK:
            self.handle_start_block(current_token)
        elif current_token.type == TOKEN.END_BLOCK:
            self.handle_end_block(current_token)
        elif current_token.type == TOKEN.WORD:
            self.handle_word(current_token)
        elif current_token.type == TOKEN.RESERVED:
            self.handle_word(current_token)
        elif current_token.type == TOKEN.SEMICOLON:
            self.handle_semicolon(current_token)
        elif current_token.type == TOKEN.STRING:
            self.handle_string(current_token)
        elif current_token.type == TOKEN.EQUALS:
            self.handle_equals(current_token)
        elif current_token.type == TOKEN.OPERATOR:
            self.handle_operator(current_token)
        elif current_token.type == TOKEN.COMMA:
            self.handle_comma(current_token)
        elif current_token.type == TOKEN.BLOCK_COMMENT:
            self.handle_block_comment(current_token, preserve_statement_flags)
        elif current_token.type == TOKEN.COMMENT:
            self.handle_comment(current_token, preserve_statement_flags)
        elif current_token.type == TOKEN.DOT:
            self.handle_dot(current_token)
        elif current_token.type == TOKEN.EOF:
            self.handle_eof(current_token)
        elif current_token.type == TOKEN.UNKNOWN:
            self.handle_unknown(current_token, preserve_statement_flags)
        else:
            self.handle_unknown(current_token, preserve_statement_flags)

    def handle_whitespace_and_comments(
        self, current_token, preserve_statement_flags=False
    ):
        newlines = current_token.newlines
        keep_whitespace = self._options.keep_array_indentation and self.is_array(
            self._flags.mode
        )

        if current_token.comments_before is not None:
            for comment_token in current_token.comments_before:
                # The cleanest handling of inline comments is to treat them
                # as though they aren't there.
                # Just continue formatting and the behavior should be logical.
                # Also ignore unknown tokens.  Again, this should result in better
                # behavior.
                self.handle_whitespace_and_comments(
                    comment_token, preserve_statement_flags
                )
                self.handle_token(comment_token, preserve_statement_flags)

        if keep_whitespace:
            for i in range(newlines):
                self.print_newline(i > 0, preserve_statement_flags)
        else:  # not keep_whitespace
            if (
                self._options.max_preserve_newlines != 0
                and newlines > self._options.max_preserve_newlines
            ):
                newlines = self._options.max_preserve_newlines

            if self._options.preserve_newlines and newlines > 1:
                self.print_newline(False, preserve_statement_flags)
                for i in range(1, newlines):
                    self.print_newline(True, preserve_statement_flags)

    def unpack(self, source, evalcode=False):
        import jsbeautifier.unpackers as unpackers

        try:
            return unpackers.run(source, evalcode)
        except unpackers.UnpackingError:
            return source

    def is_array(self, mode):
        return mode == MODE.ArrayLiteral

    def is_expression(self, mode):
        return (
            mode == MODE.Expression
            or mode == MODE.ForInitializer
            or mode == MODE.Conditional
        )

    _newline_restricted_tokens = frozenset(
        ["async", "break", "continue", "return", "throw", "yield"]
    )

    def allow_wrap_or_preserved_newline(self, current_token, force_linewrap=False):
        # never wrap the first token of a line.
        if self._output.just_added_newline():
            return

        shouldPreserveOrForce = (
            self._options.preserve_newlines and current_token.newlines
        ) or force_linewrap
        operatorLogicApplies = (
            self._flags.last_token.text in Tokenizer.positionable_operators
            or current_token.text in Tokenizer.positionable_operators
        )

        if operatorLogicApplies:
            shouldPrintOperatorNewline = (
                self._flags.last_token.text in Tokenizer.positionable_operators
                and self._options.operator_position
                in OPERATOR_POSITION_BEFORE_OR_PRESERVE
            ) or current_token.text in Tokenizer.positionable_operators
            shouldPreserveOrForce = shouldPreserveOrForce and shouldPrintOperatorNewline

        if shouldPreserveOrForce:
            self.print_newline(preserve_statement_flags=True)
        elif self._options.wrap_line_length > 0:
            if reserved_array(self._flags.last_token, self._newline_restricted_tokens):
                # These tokens should never have a newline inserted between
                # them and the following expression.
                return
            self._output.set_wrap_point()

    def print_newline(self, force_newline=False, preserve_statement_flags=False):
        if not preserve_statement_flags:
            if (
                self._flags.last_token.text != ";"
                and self._flags.last_token.text != ","
                and self._flags.last_token.text != "="
                and (
                    self._flags.last_token.type != TOKEN.OPERATOR
                    or self._flags.last_token.text == "--"
                    or self._flags.last_token.text == "++"
                )
            ):
                next_token = self._tokens.peek()
                while (
                    self._flags.mode == MODE.Statement
                    and not (self._flags.if_block and reserved_word(next_token, "else"))
                    and not self._flags.do_block
                ):
                    self.restore_mode()

        if self._output.add_new_line(force_newline):
            self._flags.multiline_frame = True

    def print_token_line_indentation(self, current_token):
        if self._output.just_added_newline():
            line = self._output.current_line
            if (
                self._options.keep_array_indentation
                and current_token.newlines
                and (self.is_array(self._flags.mode) or current_token.text == "[")
            ):
                line.set_indent(-1)
                line.push(current_token.whitespace_before)
                self._output.space_before_token = False
            elif self._output.set_indent(
                self._flags.indentation_level, self._flags.alignment
            ):
                self._flags.line_indent_level = self._flags.indentation_level

    def print_token(self, current_token, s=None):
        if self._output.raw:
            self._output.add_raw_token(current_token)
            return

        if (
            self._options.comma_first
            and current_token.previous
            and current_token.previous.type == TOKEN.COMMA
            and self._output.just_added_newline()
        ):
            if self._output.previous_line.last() == ",":
                # if the comma was already at the start of the line,
                # pull back onto that line and reprint the indentation
                popped = self._output.previous_line.pop()
                if self._output.previous_line.is_empty():
                    self._output.previous_line.push(popped)
                    self._output.trim(True)
                    self._output.current_line.pop()
                    self._output.trim()

                # add the comma in front of the next token
                self.print_token_line_indentation(current_token)
                self._output.add_token(",")
                self._output.space_before_token = True

        if s is None:
            s = current_token.text

        self.print_token_line_indentation(current_token)
        self._output.non_breaking_space = True
        self._output.add_token(s)
        if self._output.previous_token_wrapped:
            self._flags.multiline_frame = True

    def indent(self):
        self._flags.indentation_level += 1
        self._output.set_indent(self._flags.indentation_level, self._flags.alignment)

    def deindent(self):
        allow_deindent = self._flags.indentation_level > 0 and (
            (self._flags.parent is None)
            or self._flags.indentation_level > self._flags.parent.indentation_level
        )

        if allow_deindent:
            self._flags.indentation_level -= 1

        self._output.set_indent(self._flags.indentation_level, self._flags.alignment)

    def set_mode(self, mode):
        if self._flags:
            self._flag_store.append(self._flags)
            self._previous_flags = self._flags
        else:
            self._previous_flags = BeautifierFlags(mode)

        self._flags = BeautifierFlags(mode)
        self._flags.apply_base(self._previous_flags, self._output.just_added_newline())
        self._flags.start_line_index = self._output.get_line_number()

        self._output.set_indent(self._flags.indentation_level, self._flags.alignment)

    def restore_mode(self):
        if len(self._flag_store) > 0:
            self._previous_flags = self._flags
            self._flags = self._flag_store.pop()
            if self._previous_flags.mode == MODE.Statement:
                remove_redundant_indentation(self._output, self._previous_flags)

        self._output.set_indent(self._flags.indentation_level, self._flags.alignment)

    def start_of_object_property(self):
        return (
            self._flags.parent.mode == MODE.ObjectLiteral
            and self._flags.mode == MODE.Statement
            and (
                (self._flags.last_token.text == ":" and self._flags.ternary_depth == 0)
                or (reserved_array(self._flags.last_token, ["get", "set"]))
            )
        )

    def start_of_statement(self, current_token):
        start = False
        start = start or (
            reserved_array(self._flags.last_token, ["var", "let", "const"])
            and current_token.type == TOKEN.WORD
        )
        start = start or reserved_word(self._flags.last_token, "do")
        start = start or (
            not (
                self._flags.parent.mode == MODE.ObjectLiteral
                and self._flags.mode == MODE.Statement
            )
            and reserved_array(self._flags.last_token, self._newline_restricted_tokens)
            and not current_token.newlines
        )
        start = start or (
            reserved_word(self._flags.last_token, "else")
            and not (
                reserved_word(current_token, "if")
                and current_token.comments_before is None
            )
        )
        start = start or (
            self._flags.last_token.type == TOKEN.END_EXPR
            and (
                self._previous_flags.mode == MODE.ForInitializer
                or self._previous_flags.mode == MODE.Conditional
            )
        )
        start = start or (
            self._flags.last_token.type == TOKEN.WORD
            and self._flags.mode == MODE.BlockStatement
            and not self._flags.in_case
            and not (current_token.text == "--" or current_token.text == "++")
            and self._last_last_text != "function"
            and current_token.type != TOKEN.WORD
            and current_token.type != TOKEN.RESERVED
        )
        start = start or (
            self._flags.mode == MODE.ObjectLiteral
            and (
                (self._flags.last_token.text == ":" and self._flags.ternary_depth == 0)
                or (reserved_array(self._flags.last_token, ["get", "set"]))
            )
        )

        if start:
            self.set_mode(MODE.Statement)
            self.indent()

            self.handle_whitespace_and_comments(current_token, True)

            # Issue #276:
            # If starting a new statement with [if, for, while, do], push to a new line.
            # if (a) if (b) if(c) d(); else e(); else f();
            if not self.start_of_object_property():
                self.allow_wrap_or_preserved_newline(
                    current_token,
                    reserved_array(current_token, ["do", "for", "if", "while"]),
                )
            return True
        else:
            return False

    def handle_start_expr(self, current_token):
        if self.start_of_statement(current_token):
            # The conditional starts the statement if appropriate.
            pass
        else:
            self.handle_whitespace_and_comments(current_token)

        next_mode = MODE.Expression

        if current_token.text == "[":
            if (
                self._flags.last_token.type == TOKEN.WORD
                or self._flags.last_token.text == ")"
            ):
                if reserved_array(self._flags.last_token, Tokenizer.line_starters):
                    self._output.space_before_token = True
                self.print_token(current_token)
                self.set_mode(next_mode)
                self.indent()
                if self._options.space_in_paren:
                    self._output.space_before_token = True
                return

            next_mode = MODE.ArrayLiteral

            if self.is_array(self._flags.mode):
                if self._flags.last_token.text == "[" or (
                    self._flags.last_token.text == ","
                    and (self._last_last_text == "]" or self._last_last_text == "}")
                ):
                    # ], [ goes to a new line
                    # }, [ goes to a new line
                    if not self._options.keep_array_indentation:
                        self.print_newline()

            if self._flags.last_token.type not in [
                TOKEN.START_EXPR,
                TOKEN.END_EXPR,
                TOKEN.WORD,
                TOKEN.OPERATOR,
                TOKEN.DOT,
            ]:
                self._output.space_before_token = True

        else:
            if self._flags.last_token.type == TOKEN.RESERVED:
                if self._flags.last_token.text == "for":
                    self._output.space_before_token = (
                        self._options.space_before_conditional
                    )
                    next_mode = MODE.ForInitializer
                elif self._flags.last_token.text in ["if", "while", "switch"]:
                    self._output.space_before_token = (
                        self._options.space_before_conditional
                    )
                    next_mode = MODE.Conditional
                elif self._flags.last_word in ["await", "async"]:
                    # Should be a space between await and an IIFE, or async and
                    # an arrow function
                    self._output.space_before_token = True
                elif (
                    self._flags.last_token.text == "import"
                    and current_token.whitespace_before == ""
                ):
                    self._output.space_before_token = False
                elif (
                    self._flags.last_token.text in Tokenizer.line_starters
                    or self._flags.last_token.text == "catch"
                ):
                    self._output.space_before_token = True

            elif self._flags.last_token.type in [TOKEN.EQUALS, TOKEN.OPERATOR]:
                # Support of this kind of newline preservation:
                # a = (b &&
                #     (c || d));
                if not self.start_of_object_property():
                    self.allow_wrap_or_preserved_newline(current_token)
            elif self._flags.last_token.type == TOKEN.WORD:
                self._output.space_before_token = False
                # function name() vs function name ()
                # function* name() vs function* name ()
                # async name() vs async name ()
                # In ES6, you can also define the method properties of an object
                # var obj = {a: function() {}}
                # It can be abbreviated
                # var obj = {a() {}}
                # var obj = { a() {}} vs var obj = { a () {}}
                # var obj = { * a() {}} vs var obj = { * a () {}}
                peek_back_two = self._tokens.peek(-3)
                if self._options.space_after_named_function and peek_back_two:
                    # peek starts at next character so -1 is current token
                    peek_back_three = self._tokens.peek(-4)
                    if reserved_array(peek_back_two, ["async", "function"]) or (
                        peek_back_two.text == "*"
                        and reserved_array(peek_back_three, ["async", "function"])
                    ):
                        self._output.space_before_token = True
                    elif self._flags.mode == MODE.ObjectLiteral:
                        if (peek_back_two.text == "{" or peek_back_two.text == ",") or (
                            peek_back_two.text == "*"
                            and (
                                peek_back_three.text == "{"
                                or peek_back_three.text == ","
                            )
                        ):
                            self._output.space_before_token = True
                    elif self._flags.parent and self._flags.parent.class_start_block:
                        self._output.space_before_token = True
            else:
                # Support preserving wrapped arrow function expressions
                # a.b('c',
                #     () => d.e
                # )
                self.allow_wrap_or_preserved_newline(current_token)

            # function() vs function (), typeof() vs typeof ()
            # function*() vs function* (), yield*() vs yield* ()
            if (
                self._flags.last_token.type == TOKEN.RESERVED
                and (
                    self._flags.last_word == "function"
                    or self._flags.last_word == "typeof"
                )
            ) or (
                self._flags.last_token.text == "*"
                and (
                    self._last_last_text in ["function", "yield"]
                    or (
                        self._flags.mode == MODE.ObjectLiteral
                        and self._last_last_text in ["{", ","]
                    )
                )
            ):
                self._output.space_before_token = (
                    self._options.space_after_anon_function
                )

        if (
            self._flags.last_token.text == ";"
            or self._flags.last_token.type == TOKEN.START_BLOCK
        ):
            self.print_newline()
        elif (
            self._flags.last_token.type
            in [TOKEN.END_EXPR, TOKEN.START_EXPR, TOKEN.END_BLOCK, TOKEN.COMMA]
            or self._flags.last_token.text == "."
        ):
            # do nothing on (( and )( and ][ and ]( and .(
            # TODO: Consider whether forcing this is required.  Review failing
            # tests when removed.
            self.allow_wrap_or_preserved_newline(current_token, current_token.newlines)

        self.print_token(current_token)
        self.set_mode(next_mode)

        if self._options.space_in_paren:
            self._output.space_before_token = True

        # In all cases, if we newline while inside an expression it should be
        # indented.
        self.indent()

    def handle_end_expr(self, current_token):
        # statements inside expressions are not valid syntax, but...
        # statements must all be closed when their container closes
        while self._flags.mode == MODE.Statement:
            self.restore_mode()

        self.handle_whitespace_and_comments(current_token)

        if self._flags.multiline_frame:
            self.allow_wrap_or_preserved_newline(
                current_token,
                current_token.text == "]"
                and self.is_array(self._flags.mode)
                and not self._options.keep_array_indentation,
            )

        if self._options.space_in_paren:
            if (
                self._flags.last_token.type == TOKEN.START_EXPR
                and not self._options.space_in_empty_paren
            ):
                # empty parens are always "()" and "[]", not "( )" or "[ ]"
                self._output.space_before_token = False
                self._output.trim()
            else:
                self._output.space_before_token = True

        self.deindent()
        self.print_token(current_token)
        self.restore_mode()

        remove_redundant_indentation(self._output, self._previous_flags)

        # do {} while () // no statement required after
        if self._flags.do_while and self._previous_flags.mode == MODE.Conditional:
            self._previous_flags.mode = MODE.Expression
            self._flags.do_block = False
            self._flags.do_while = False

    def handle_start_block(self, current_token):
        self.handle_whitespace_and_comments(current_token)

        # Check if this is a BlockStatement that should be treated as a
        # ObjectLiteral
        next_token = self._tokens.peek()
        second_token = self._tokens.peek(1)
        if (
            self._flags.last_word == "switch"
            and self._flags.last_token.type == TOKEN.END_EXPR
        ):
            self.set_mode(MODE.BlockStatement)
            self._flags.in_case_statement = True
        elif self._flags.case_body:
            self.set_mode(MODE.BlockStatement)
        elif second_token is not None and (
            (
                second_token.text in [":", ","]
                and next_token.type in [TOKEN.STRING, TOKEN.WORD, TOKEN.RESERVED]
            )
            or (
                next_token.text in ["get", "set", "..."]
                and second_token.type in [TOKEN.WORD, TOKEN.RESERVED]
            )
        ):
            # We don't support TypeScript,but we didn't break it for a very long time.
            # We'll try to keep not breaking it.
            if self._last_last_text in [
                "class",
                "interface",
            ] and second_token.text not in [":", ","]:
                self.set_mode(MODE.BlockStatement)
            else:
                self.set_mode(MODE.ObjectLiteral)
        elif (
            self._flags.last_token.type == TOKEN.OPERATOR
            and self._flags.last_token.text == "=>"
        ):
            # arrow function: (param1, paramN) => { statements }
            self.set_mode(MODE.BlockStatement)
        elif self._flags.last_token.type in [
            TOKEN.EQUALS,
            TOKEN.START_EXPR,
            TOKEN.COMMA,
            TOKEN.OPERATOR,
        ] or reserved_array(
            self._flags.last_token, ["return", "throw", "import", "default"]
        ):
            # Detecting shorthand function syntax is difficult by scanning forward,
            #     so check the surrounding context.
            # If the block is being returned, imported, export default, passed as arg,
            # assigned with = or assigned in a nested object, treat as an
            # ObjectLiteral.
            self.set_mode(MODE.ObjectLiteral)
        else:
            self.set_mode(MODE.BlockStatement)

        if self._flags.last_token:
            if reserved_array(self._flags.last_token.previous, ["class", "extends"]):
                self._flags.class_start_block = True

        empty_braces = (
            (next_token is not None)
            and next_token.comments_before is None
            and next_token.text == "}"
        )
        empty_anonymous_function = (
            empty_braces
            and self._flags.last_word == "function"
            and self._flags.last_token.type == TOKEN.END_EXPR
        )

        if (
            self._options.brace_preserve_inline
        ):  # check for inline, set inline_frame if so
            # search forward for newline wanted inside this block
            index = 0
            check_token = None
            self._flags.inline_frame = True
            do_loop = True
            while do_loop:
                index += 1
                check_token = self._tokens.peek(index - 1)
                if check_token.newlines:
                    self._flags.inline_frame = False

                do_loop = check_token.type != TOKEN.EOF and not (
                    check_token.type == TOKEN.END_BLOCK
                    and check_token.opened == current_token
                )

        if (
            self._options.brace_style == "expand"
            or (self._options.brace_style == "none" and current_token.newlines)
        ) and not self._flags.inline_frame:
            if self._flags.last_token.type != TOKEN.OPERATOR and (
                empty_anonymous_function
                or self._flags.last_token.type == TOKEN.EQUALS
                or (
                    reserved_array(self._flags.last_token, _special_word_set)
                    and self._flags.last_token.text != "else"
                )
            ):
                self._output.space_before_token = True
            else:
                self.print_newline(preserve_statement_flags=True)
        else:  # collapse || inline_frame
            if self.is_array(self._previous_flags.mode) and (
                self._flags.last_token.type == TOKEN.START_EXPR
                or self._flags.last_token.type == TOKEN.COMMA
            ):
                # if we're preserving inline,
                # allow newline between comma and next brace.
                if self._flags.inline_frame:
                    self.allow_wrap_or_preserved_newline(current_token)
                    self._flags.inline_frame = True
                    self._previous_flags.multiline_frame = (
                        self._previous_flags.multiline_frame
                        or self._flags.multiline_frame
                    )
                    self._flags.multiline_frame = False
                elif self._flags.last_token.type == TOKEN.COMMA:
                    self._output.space_before_token = True

            elif self._flags.last_token.type not in [TOKEN.OPERATOR, TOKEN.START_EXPR]:
                if (
                    self._flags.last_token.type in [TOKEN.START_BLOCK, TOKEN.SEMICOLON]
                    and not self._flags.inline_frame
                ):
                    self.print_newline()
                else:
                    self._output.space_before_token = True

        self.print_token(current_token)
        self.indent()

        # Except for specific cases, open braces are followed by a new line.
        if not empty_braces and not (
            self._options.brace_preserve_inline and self._flags.inline_frame
        ):
            self.print_newline()

    def handle_end_block(self, current_token):
        # statements must all be closed when their container closes
        self.handle_whitespace_and_comments(current_token)

        while self._flags.mode == MODE.Statement:
            self.restore_mode()

        empty_braces = self._flags.last_token.type == TOKEN.START_BLOCK

        # try inline_frame (only set if opt.braces-preserve-inline) first
        if self._flags.inline_frame and not empty_braces:
            self._output.space_before_token = True
        elif self._options.brace_style == "expand":
            if not empty_braces:
                self.print_newline()
        else:
            # skip {}
            if not empty_braces:
                if (
                    self.is_array(self._flags.mode)
                    and self._options.keep_array_indentation
                ):
                    self._options.keep_array_indentation = False
                    self.print_newline()
                    self._options.keep_array_indentation = True
                else:
                    self.print_newline()

        self.restore_mode()
        self.print_token(current_token)

    def handle_word(self, current_token):
        if current_token.type == TOKEN.RESERVED:
            if (
                current_token.text in ["set", "get"]
                and self._flags.mode != MODE.ObjectLiteral
            ):
                current_token.type = TOKEN.WORD
            elif current_token.text == "import" and self._tokens.peek().text in [
                "(",
                ".",
            ]:
                current_token.type = TOKEN.WORD
            elif current_token.text in ["as", "from"] and not self._flags.import_block:
                current_token.type = TOKEN.WORD
            elif self._flags.mode == MODE.ObjectLiteral:
                next_token = self._tokens.peek()
                if next_token.text == ":":
                    current_token.type = TOKEN.WORD

        if self.start_of_statement(current_token):
            # The conditional starts the statement if appropriate.
            if (
                reserved_array(self._flags.last_token, ["var", "let", "const"])
                and current_token.type == TOKEN.WORD
            ):
                self._flags.declaration_statement = True

        elif (
            current_token.newlines
            and not self.is_expression(self._flags.mode)
            and (
                self._flags.last_token.type != TOKEN.OPERATOR
                or (
                    self._flags.last_token.text == "--"
                    or self._flags.last_token.text == "++"
                )
            )
            and self._flags.last_token.type != TOKEN.EQUALS
            and (
                self._options.preserve_newlines
                or not reserved_array(
                    self._flags.last_token, ["var", "let", "const", "set", "get"]
                )
            )
        ):
            self.handle_whitespace_and_comments(current_token)
            self.print_newline()
        else:
            self.handle_whitespace_and_comments(current_token)

        if self._flags.do_block and not self._flags.do_while:
            if reserved_word(current_token, "while"):
                # do {} ## while ()
                self._output.space_before_token = True
                self.print_token(current_token)
                self._output.space_before_token = True
                self._flags.do_while = True
                return
            else:
                # do {} should always have while as the next word.
                # if we don't see the expected while, recover
                self.print_newline()
                self._flags.do_block = False

        # if may be followed by else, or not
        # Bare/inline ifs are tricky
        # Need to unwind the modes correctly: if (a) if (b) c(); else d(); else
        # e();
        if self._flags.if_block:
            if (not self._flags.else_block) and reserved_word(current_token, "else"):
                self._flags.else_block = True
            else:
                while self._flags.mode == MODE.Statement:
                    self.restore_mode()

                self._flags.if_block = False

        if self._flags.in_case_statement and reserved_array(
            current_token, ["case", "default"]
        ):
            self.print_newline()
            if (not self._flags.case_block) and (
                self._flags.case_body or self._options.jslint_happy
            ):
                self.deindent()
            self._flags.case_body = False
            self.print_token(current_token)
            self._flags.in_case = True
            return

        if self._flags.last_token.type in [
            TOKEN.COMMA,
            TOKEN.START_EXPR,
            TOKEN.EQUALS,
            TOKEN.OPERATOR,
        ]:
            if not self.start_of_object_property() and not (
                # start of object property is different for numeric values with +/- prefix operators
                self._flags.last_token.text in ["+", "-"]
                and self._last_last_text == ":"
                and self._flags.parent.mode == MODE.ObjectLiteral
            ):
                self.allow_wrap_or_preserved_newline(current_token)

        if reserved_word(current_token, "function"):
            if self._flags.last_token.text in ["}", ";"] or (
                self._output.just_added_newline()
                and not (
                    self._flags.last_token.text in ["(", "[", "{", ":", "=", ","]
                    or self._flags.last_token.type == TOKEN.OPERATOR
                )
            ):
                # make sure there is a nice clean space of at least one blank line
                # before a new function definition, except in arrays
                if (
                    not self._output.just_added_blankline()
                    and current_token.comments_before is None
                ):
                    self.print_newline()
                    self.print_newline(True)

            if (
                self._flags.last_token.type == TOKEN.RESERVED
                or self._flags.last_token.type == TOKEN.WORD
            ):
                if reserved_array(
                    self._flags.last_token, ["get", "set", "new", "export"]
                ) or reserved_array(
                    self._flags.last_token, self._newline_restricted_tokens
                ):
                    self._output.space_before_token = True
                elif (
                    reserved_word(self._flags.last_token, "default")
                    and self._last_last_text == "export"
                ):
                    self._output.space_before_token = True
                elif self._flags.last_token.text == "declare":
                    # accomodates Typescript declare function formatting
                    self._output.space_before_token = True
                else:
                    self.print_newline()
            elif (
                self._flags.last_token.type == TOKEN.OPERATOR
                or self._flags.last_token.text == "="
            ):
                # foo = function
                self._output.space_before_token = True
            elif not self._flags.multiline_frame and (
                self.is_expression(self._flags.mode) or self.is_array(self._flags.mode)
            ):
                # (function
                pass
            else:
                self.print_newline()

            self.print_token(current_token)
            self._flags.last_word = current_token.text
            return

        prefix = "NONE"

        if self._flags.last_token.type == TOKEN.END_BLOCK:
            if self._previous_flags.inline_frame:
                prefix = "SPACE"
            elif not reserved_array(
                current_token, ["else", "catch", "finally", "from"]
            ):
                prefix = "NEWLINE"
            else:
                if self._options.brace_style in ["expand", "end-expand"] or (
                    self._options.brace_style == "none" and current_token.newlines
                ):
                    prefix = "NEWLINE"
                else:
                    prefix = "SPACE"
                    self._output.space_before_token = True
        elif (
            self._flags.last_token.type == TOKEN.SEMICOLON
            and self._flags.mode == MODE.BlockStatement
        ):
            # TODO: Should this be for STATEMENT as well?
            prefix = "NEWLINE"
        elif self._flags.last_token.type == TOKEN.SEMICOLON and self.is_expression(
            self._flags.mode
        ):
            prefix = "SPACE"
        elif self._flags.last_token.type == TOKEN.STRING:
            prefix = "NEWLINE"
        elif (
            self._flags.last_token.type == TOKEN.RESERVED
            or self._flags.last_token.type == TOKEN.WORD
            or (
                self._flags.last_token.text == "*"
                and (
                    self._last_last_text in ["function", "yield"]
                    or (
                        self._flags.mode == MODE.ObjectLiteral
                        and self._last_last_text in ["{", ","]
                    )
                )
            )
        ):
            prefix = "SPACE"
        elif self._flags.last_token.type == TOKEN.START_BLOCK:
            if self._flags.inline_frame:
                prefix = "SPACE"
            else:
                prefix = "NEWLINE"
        elif self._flags.last_token.type == TOKEN.END_EXPR:
            self._output.space_before_token = True
            prefix = "NEWLINE"

        if (
            reserved_array(current_token, Tokenizer.line_starters)
            and self._flags.last_token.text != ")"
        ):
            if (
                self._flags.inline_frame
                or self._flags.last_token.text == "else "
                or self._flags.last_token.text == "export"
            ):
                prefix = "SPACE"
            else:
                prefix = "NEWLINE"

        if reserved_array(current_token, ["else", "catch", "finally"]):
            if (
                (
                    not (
                        self._flags.last_token.type == TOKEN.END_BLOCK
                        and self._previous_flags.mode == MODE.BlockStatement
                    )
                )
                or self._options.brace_style == "expand"
                or self._options.brace_style == "end-expand"
                or (self._options.brace_style == "none" and current_token.newlines)
            ) and not self._flags.inline_frame:
                self.print_newline()
            else:
                self._output.trim(True)
                # If we trimmed and there's something other than a close block before us
                # put a newline back in.  Handles '} // comment' scenario.
                if self._output.current_line.last() != "}":
                    self.print_newline()

                self._output.space_before_token = True

        elif prefix == "NEWLINE":
            if reserved_array(self._flags.last_token, _special_word_set):
                # no newline between return nnn
                self._output.space_before_token = True
            elif self._flags.last_token.text == "declare" and reserved_array(
                current_token, ["var", "let", "const"]
            ):
                # accomodates Typescript declare formatting
                self._output.space_before_token = True
            elif self._flags.last_token.type != TOKEN.END_EXPR:
                if (
                    self._flags.last_token.type != TOKEN.START_EXPR
                    or not (reserved_array(current_token, ["var", "let", "const"]))
                ) and self._flags.last_token.text != ":":
                    # no need to force newline on VAR -
                    # for (var x = 0...
                    if (
                        reserved_word(current_token, "if")
                        and self._flags.last_token.text == "else"
                    ):
                        self._output.space_before_token = True
                    else:
                        self.print_newline()
            elif (
                reserved_array(current_token, Tokenizer.line_starters)
                and self._flags.last_token.text != ")"
            ):
                self.print_newline()
        elif (
            self._flags.multiline_frame
            and self.is_array(self._flags.mode)
            and self._flags.last_token.text == ","
            and self._last_last_text == "}"
        ):
            self.print_newline()  # }, in lists get a newline
        elif prefix == "SPACE":
            self._output.space_before_token = True

        if current_token.previous and (
            current_token.previous.type == TOKEN.WORD
            or current_token.previous.type == TOKEN.RESERVED
        ):
            self._output.space_before_token = True

        self.print_token(current_token)
        self._flags.last_word = current_token.text

        if current_token.type == TOKEN.RESERVED:
            if current_token.text == "do":
                self._flags.do_block = True
            elif current_token.text == "if":
                self._flags.if_block = True
            elif current_token.text == "import":
                self._flags.import_block = True
            elif current_token.text == "from" and self._flags.import_block:
                self._flags.import_block = False

    def handle_semicolon(self, current_token):
        if self.start_of_statement(current_token):
            # The conditional starts the statement if appropriate.
            # Semicolon can be the start (and end) of a statement
            self._output.space_before_token = False
        else:
            self.handle_whitespace_and_comments(current_token)

        next_token = self._tokens.peek()
        while (
            self._flags.mode == MODE.Statement
            and not (self._flags.if_block and reserved_word(next_token, "else"))
            and not self._flags.do_block
        ):
            self.restore_mode()

        if self._flags.import_block:
            self._flags.import_block = False

        self.print_token(current_token)

    def handle_string(self, current_token):
        if (
            current_token.text[0] == "`"
            and current_token.newlines == 0
            and current_token.whitespace_before == ""
            and (
                self._flags.last_token.type == TOKEN.WORD
                or current_token.previous.text == ")"
            )
        ):
            # This conditional checks backtick strings and makes no changes
            pass
        elif self.start_of_statement(current_token):
            # The conditional starts the statement if appropriate.
            # One difference - strings want at least a space before
            self._output.space_before_token = True
        else:
            self.handle_whitespace_and_comments(current_token)
            if (
                self._flags.last_token.type in [TOKEN.RESERVED, TOKEN.WORD]
                or self._flags.inline_frame
            ):
                self._output.space_before_token = True
            elif self._flags.last_token.type in [
                TOKEN.COMMA,
                TOKEN.START_EXPR,
                TOKEN.EQUALS,
                TOKEN.OPERATOR,
            ]:
                if not self.start_of_object_property():
                    self.allow_wrap_or_preserved_newline(current_token)
            elif (
                current_token.text[0] == "`"
                and self._flags.last_token.type == TOKEN.END_EXPR
                and current_token.previous.text in ["]", ")"]
                and current_token.newlines == 0
            ):
                self._output.space_before_token = True
            else:
                self.print_newline()

        self.print_token(current_token)

    def handle_equals(self, current_token):
        if self.start_of_statement(current_token):
            # The conditional starts the statement if appropriate.
            pass
        else:
            self.handle_whitespace_and_comments(current_token)

        if self._flags.declaration_statement:
            # just got an '=' in a var-line, different line breaking rules will
            # apply
            self._flags.declaration_assignment = True

        self._output.space_before_token = True
        self.print_token(current_token)
        self._output.space_before_token = True

    def handle_comma(self, current_token):
        self.handle_whitespace_and_comments(current_token, True)

        self.print_token(current_token)
        self._output.space_before_token = True

        if self._flags.declaration_statement:
            if self.is_expression(self._flags.parent.mode):
                # do not break on comma, for ( var a = 1, b = 2
                self._flags.declaration_assignment = False

            if self._flags.declaration_assignment:
                self._flags.declaration_assignment = False
                self.print_newline(preserve_statement_flags=True)
            elif self._options.comma_first:
                # for comma-first, we want to allow a newline before the comma
                # to turn into a newline after the comma, which we will fixup
                # later
                self.allow_wrap_or_preserved_newline(current_token)

        elif self._flags.mode == MODE.ObjectLiteral or (
            self._flags.mode == MODE.Statement
            and self._flags.parent.mode == MODE.ObjectLiteral
        ):
            if self._flags.mode == MODE.Statement:
                self.restore_mode()

            if not self._flags.inline_frame:
                self.print_newline()
        elif self._options.comma_first:
            # EXPR or DO_BLOCK
            # for comma-first, we want to allow a newline before the comma
            # to turn into a newline after the comma, which we will fixup later
            self.allow_wrap_or_preserved_newline(current_token)

    def handle_operator(self, current_token):
        isGeneratorAsterisk = current_token.text == "*" and (
            reserved_array(self._flags.last_token, ["function", "yield"])
            or (
                self._flags.last_token.type
                in [TOKEN.START_BLOCK, TOKEN.COMMA, TOKEN.END_BLOCK, TOKEN.SEMICOLON]
            )
        )
        isUnary = current_token.text in ["+", "-"] and (
            self._flags.last_token.type
            in [TOKEN.START_BLOCK, TOKEN.START_EXPR, TOKEN.EQUALS, TOKEN.OPERATOR]
            or self._flags.last_token.text in Tokenizer.line_starters
            or self._flags.last_token.text == ","
        )

        if self.start_of_statement(current_token):
            # The conditional starts the statement if appropriate.
            pass
        else:
            preserve_statement_flags = not isGeneratorAsterisk
            self.handle_whitespace_and_comments(current_token, preserve_statement_flags)

        # hack for actionscript's import .*;
        if current_token.text == "*" and self._flags.last_token.type == TOKEN.DOT:
            self.print_token(current_token)
            return

        if current_token.text == "::":
            # no spaces around the exotic namespacing syntax operator
            self.print_token(current_token)
            return

        if current_token.text in ["-", "+"] and self.start_of_object_property():
            # numeric value with +/- symbol in front as a property
            self.print_token(current_token)
            return

        # Allow line wrapping between operators when operator_position is
        #   set to before or preserve
        if (
            self._flags.last_token.type == TOKEN.OPERATOR
            and self._options.operator_position in OPERATOR_POSITION_BEFORE_OR_PRESERVE
        ):
            self.allow_wrap_or_preserved_newline(current_token)

        if current_token.text == ":" and self._flags.in_case:
            self.print_token(current_token)
            self._flags.in_case = False
            self._flags.case_body = True
            if self._tokens.peek().type != TOKEN.START_BLOCK:
                self.indent()
                self.print_newline()
                self._flags.case_block = False
            else:
                self._output.space_before_token = True
                self._flags.case_block = True

            return

        space_before = True
        space_after = True
        in_ternary = False

        if current_token.text == ":":
            if self._flags.ternary_depth == 0:
                # Colon is invalid javascript outside of ternary and object,
                # but do our best to guess what was meant.
                space_before = False
            else:
                self._flags.ternary_depth -= 1
                in_ternary = True
        elif current_token.text == "?":
            self._flags.ternary_depth += 1

        # let's handle the operator_position option prior to any conflicting
        # logic
        if (
            (not isUnary)
            and (not isGeneratorAsterisk)
            and self._options.preserve_newlines
            and current_token.text in Tokenizer.positionable_operators
        ):
            isColon = current_token.text == ":"
            isTernaryColon = isColon and in_ternary
            isOtherColon = isColon and not in_ternary

            if self._options.operator_position == OPERATOR_POSITION["before_newline"]:
                # if the current token is : and it's not a ternary statement
                # then we set space_before to false
                self._output.space_before_token = not isOtherColon

                self.print_token(current_token)

                if (not isColon) or isTernaryColon:
                    self.allow_wrap_or_preserved_newline(current_token)

                self._output.space_before_token = True

                return

            elif self._options.operator_position == OPERATOR_POSITION["after_newline"]:
                # if the current token is anything but colon, or (via deduction) it's a colon and in a ternary statement,
                #   then print a newline.
                self._output.space_before_token = True

                if (not isColon) or isTernaryColon:
                    if self._tokens.peek().newlines:
                        self.print_newline(preserve_statement_flags=True)
                    else:
                        self.allow_wrap_or_preserved_newline(current_token)
                else:
                    self._output.space_before_token = False

                self.print_token(current_token)

                self._output.space_before_token = True
                return

            elif (
                self._options.operator_position == OPERATOR_POSITION["preserve_newline"]
            ):
                if not isOtherColon:
                    self.allow_wrap_or_preserved_newline(current_token)

                # if we just added a newline, or the current token is : and it's not a ternary statement,
                #   then we set space_before to false
                self._output.space_before_token = not (
                    self._output.just_added_newline() or isOtherColon
                )

                self.print_token(current_token)

                self._output.space_before_token = True
                return

        if isGeneratorAsterisk:
            self.allow_wrap_or_preserved_newline(current_token)
            space_before = False
            next_token = self._tokens.peek()
            space_after = next_token and next_token.type in [TOKEN.WORD, TOKEN.RESERVED]
        elif current_token.text == "...":
            self.allow_wrap_or_preserved_newline(current_token)
            space_before = self._flags.last_token.type == TOKEN.START_BLOCK
            space_after = False
        elif current_token.text in ["--", "++", "!", "~"] or isUnary:
            if (
                self._flags.last_token.type == TOKEN.COMMA
                or self._flags.last_token.type == TOKEN.START_EXPR
            ):
                self.allow_wrap_or_preserved_newline(current_token)

            space_before = False
            space_after = False

            # http://www.ecma-international.org/ecma-262/5.1/#sec-7.9.1
            # if there is a newline between -- or ++ and anything else we
            # should preserve it.
            if current_token.newlines and (
                current_token.text == "--"
                or current_token.text == "++"
                or current_token.text == "~"
            ):
                new_line_needed = (
                    reserved_array(self._flags.last_token, _special_word_set)
                    and current_token.newlines
                )
                if new_line_needed and (
                    self._previous_flags.if_block or self._previous_flags.else_block
                ):
                    self.restore_mode()
                self.print_newline(new_line_needed, True)

            if self._flags.last_token.text == ";" and self.is_expression(
                self._flags.mode
            ):
                # for (;; ++i)
                #         ^^
                space_before = True

            if self._flags.last_token.type == TOKEN.RESERVED:
                space_before = True
            elif self._flags.last_token.type == TOKEN.END_EXPR:
                space_before = not (
                    self._flags.last_token.text == "]"
                    and current_token.text in ["--", "++"]
                )
            elif self._flags.last_token.type == TOKEN.OPERATOR:
                # a++ + ++b
                # a - -b
                space_before = current_token.text in [
                    "--",
                    "-",
                    "++",
                    "+",
                ] and self._flags.last_token.text in ["--", "-", "++", "+"]
                # + and - are not unary when preceeded by -- or ++ operator
                # a-- + b
                # a * +b
                # a - -b
                if current_token.text in ["-", "+"] and self._flags.last_token.text in [
                    "--",
                    "++",
                ]:
                    space_after = True

            if (
                (
                    self._flags.mode == MODE.BlockStatement
                    and not self._flags.inline_frame
                )
                or self._flags.mode == MODE.Statement
            ) and self._flags.last_token.text in ["{", ";"]:
                # { foo: --i }
                # foo(): --bar
                self.print_newline()

        if space_before:
            self._output.space_before_token = True

        self.print_token(current_token)

        if space_after:
            self._output.space_before_token = True

    def handle_block_comment(self, current_token, preserve_statement_flags):
        if self._output.raw:
            self._output.add_raw_token(current_token)
            if (
                current_token.directives
                and current_token.directives.get("preserve") == "end"
            ):
                # If we're testing the raw output behavior, do not allow a
                # directive to turn it off.
                self._output.raw = self._options.test_output_raw
            return

        if current_token.directives:
            self.print_newline(preserve_statement_flags=preserve_statement_flags)
            self.print_token(current_token)
            if current_token.directives.get("preserve") == "start":
                self._output.raw = True

            self.print_newline(preserve_statement_flags=True)
            return

        # inline block
        if (
            not self.acorn.newline.search(current_token.text)
            and not current_token.newlines
        ):
            self._output.space_before_token = True
            self.print_token(current_token)
            self._output.space_before_token = True
            return

        lines = self.acorn.allLineBreaks.split(current_token.text)
        javadoc = False
        starless = False
        last_indent = current_token.whitespace_before
        last_indent_length = len(last_indent)

        # block comment starts with a new line
        self.print_newline(preserve_statement_flags=preserve_statement_flags)

        # first line always indented
        self.print_token(current_token, lines[0])
        self.print_newline(preserve_statement_flags=preserve_statement_flags)

        if len(lines) > 1:
            lines = lines[1:]
            javadoc = not any(
                l for l in lines if (l.strip() == "" or (l.lstrip())[0] != "*")
            )
            starless = all(l.startswith(last_indent) or l.strip() == "" for l in lines)

            if javadoc:
                self._flags.alignment = 1

            for line in lines:
                if javadoc:
                    # javadoc: reformat and re-indent
                    self.print_token(current_token, line.lstrip())
                elif starless and len(line) > last_indent_length:
                    # starless: re-indent non-empty content, avoiding trim
                    self.print_token(current_token, line[last_indent_length:])
                else:
                    # normal comments output raw
                    self._output.current_line.set_indent(-1)
                    self._output.add_token(line)

                # for comments on their own line or  more than one line,
                # make sure there's a new line after
                self.print_newline(preserve_statement_flags=preserve_statement_flags)

            self._flags.alignment = 0

    def handle_comment(self, current_token, preserve_statement_flags):
        if current_token.newlines:
            self.print_newline(preserve_statement_flags=preserve_statement_flags)

        if not current_token.newlines:
            self._output.trim(True)

        self._output.space_before_token = True
        self.print_token(current_token)
        self.print_newline(preserve_statement_flags=preserve_statement_flags)

    def handle_dot(self, current_token):
        if self.start_of_statement(current_token):
            # The conditional starts the statement if appropriate.
            pass
        else:
            self.handle_whitespace_and_comments(current_token, True)

        if re.search("^([0-9])+$", self._flags.last_token.text):
            self._output.space_before_token = True

        if reserved_array(self._flags.last_token, _special_word_set):
            self._output.space_before_token = False
        else:
            # allow preserved newlines before dots in general
            # force newlines on dots after close paren when break_chained - for
            # bar().baz()
            self.allow_wrap_or_preserved_newline(
                current_token,
                self._flags.last_token.text == ")"
                and self._options.break_chained_methods,
            )

        # Only unindent chained method dot if this dot starts a new line.
        # Otherwise the automatic extra indentation removal
        # will handle any over indent
        if self._options.unindent_chained_methods and self._output.just_added_newline():
            self.deindent()

        self.print_token(current_token)

    def handle_unknown(self, current_token, preserve_statement_flags):
        self.print_token(current_token)
        if current_token.text[-1] == "\n":
            self.print_newline(preserve_statement_flags=preserve_statement_flags)

    def handle_eof(self, current_token):
        # Unwind any open statements
        while self._flags.mode == MODE.Statement:
            self.restore_mode()

        self.handle_whitespace_and_comments(current_token)
