From 5a2879b2b1d06a6aa7e643718e42ebd046a1e903 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jul 14 2020 12:36:04 +0000 Subject: Introducing the boards feature This feature introduce the presentation of tickets in some sort of light weight kanban board. When creating a board, it is associated with a tag of the project. Adding ticket to this tag will automatically add the ticket to the board (and vice-versa, un-tagging will remove it). The boards can have any number of status (columns) which are defined by the user but one of them must be marked as the default status to which tickets are added when they are tagged. Some status can be marked as "Close" in which case they will close the ticket when it reaches that status. If a "Close status" is specified, that close status will be applied when closing the ticket. Signed-off-by: Pierre-Yves Chibon --- diff --git a/alembic/versions/060b20d6d6e6_add_boards.py b/alembic/versions/060b20d6d6e6_add_boards.py new file mode 100644 index 0000000..3a4f5d7 --- /dev/null +++ b/alembic/versions/060b20d6d6e6_add_boards.py @@ -0,0 +1,106 @@ +"""Add boards + +Revision ID: 060b20d6d6e6 +Revises: d7589827abbb +Create Date: 2020-06-03 09:38:36.189205 + +""" + +import datetime + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '060b20d6d6e6' +down_revision = 'd7589827abbb' + + +def upgrade(): + op.create_table( + 'boards', + sa.Column('id', sa.INTEGER(), nullable=False, autoincrement=True), + sa.Column('project_id', sa.INTEGER(), nullable=False), + sa.Column('tag_id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.TEXT(), nullable=False), + sa.Column('active', sa.BOOLEAN(), nullable=False, default=True), + sa.Column('created', sa.DateTime(), nullable=False, default=datetime.datetime.utcnow), + sa.Column('date_updated', sa.DateTime(), nullable=False, + default=datetime.datetime.utcnow, + onupdate=datetime.datetime.utcnow), + sa.ForeignKeyConstraint( + ['project_id'], + [u'projects.id'], + name=u'boards_project_id_fkey', + onupdate=u'CASCADE', + ondelete=u'CASCADE' + ), + sa.ForeignKeyConstraint( + ['tag_id'], + [u'tags_colored.id'], + name=u'boards_tag_id_fkey', + onupdate=u'CASCADE' + ), + sa.PrimaryKeyConstraint('id', name=u'boards_pkey'), + sa.UniqueConstraint( + "project_id", "name", name="boards_project_id_name_uix" + ), + ) + + op.create_table( + 'board_statuses', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('board_id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.String(128), nullable=False), + sa.Column('rank', sa.INTEGER, nullable=False), + sa.Column('default', sa.Boolean, nullable=False, default=False), + sa.Column('bg_color', sa.String(32), nullable=False), + sa.Column('close', sa.BOOLEAN(), nullable=False, default=False), + sa.Column('close_status', sa.Text(), nullable=True), + sa.Column('created', sa.DateTime(), nullable=False, default=datetime.datetime.utcnow), + sa.ForeignKeyConstraint( + ['board_id'], + [u'boards.id'], + name=u'board_statuses_board_id_fkey', + onupdate=u'CASCADE', + ondelete=u'CASCADE' + ), + sa.PrimaryKeyConstraint('id', name=u'board_statuses_pkey'), + sa.UniqueConstraint( + "board_id", "name", name="board_statuses_board_id_name_uix" + ), + ) + + op.create_table( + 'boards_issues', + sa.Column('issue_uid', sa.String(32), nullable=False), + sa.Column('status_id', sa.INTEGER(), nullable=False), + sa.Column('rank', sa.INTEGER(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False, default=datetime.datetime.utcnow), + sa.ForeignKeyConstraint( + ['issue_uid'], + [u'issues.uid'], + name=u'boards_issues_issue_uid_fkey', + onupdate=u'CASCADE', + ondelete=u'CASCADE' + ), + sa.ForeignKeyConstraint( + ['status_id'], + [u'board_statuses.id'], + name=u'boards_issues_status_id_fkey', + onupdate=u'CASCADE', + ondelete=u'CASCADE' + ), + sa.PrimaryKeyConstraint('status_id', "issue_uid", name=u'boards_issues_pkey'), + sa.UniqueConstraint( + "status_id", "issue_uid", name="boards_issues_status_id_issue_uid_uix" + ), + ) + + +def downgrade(): + """ Remove the tables related to the boards feature. """ + op.drop_table('boards_issues') + op.drop_table('board_statuses') + op.drop_table('boards') diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index 9db448e..3523fee 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -326,6 +326,7 @@ def get_per_page(): if pagure_config.get("ENABLE_TICKETS", True): from pagure.api import issue # noqa: E402 + from pagure.api import boards # noqa: E402, F401 from pagure.api import fork # noqa: E402 from pagure.api import project # noqa: E402 from pagure.api import user # noqa: E402 diff --git a/pagure/api/boards.py b/pagure/api/boards.py new file mode 100644 index 0000000..e42f7cb --- /dev/null +++ b/pagure/api/boards.py @@ -0,0 +1,786 @@ +# -*- coding: utf-8 -*- + +""" + (c) 2020 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +from __future__ import unicode_literals, absolute_import + +import logging + +import flask +from sqlalchemy.exc import SQLAlchemyError +import werkzeug.datastructures + +import pagure +import pagure.exceptions +import pagure.lib.query +import pagure.lib.tasks +from pagure.forms import TAGS_REGEX, TAGS_REGEX_RE +from pagure.api import ( + API, + api_method, + api_login_required, + APIERROR, + get_request_data, +) +from pagure.api.utils import ( + _get_repo, + _check_token, + _check_issue_tracker, +) + + +_log = logging.getLogger(__name__) + + +@API.route("//boards") +@API.route("///boards") +@API.route("/fork///boards") +@API.route("/fork////boards") +@api_method +def api_boards_view(repo, username=None, namespace=None): + """ + List a project's boards + ----------------------- + Retrieve the list of boards a project has. + + :: + + GET /api/0//boards + GET /api/0///boards + + :: + + GET /api/0/fork///boards + GET /api/0/fork////boards + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "total_boards": 3, + "boards": [ + {"name": "infrastructure", "active": true}, + {"name": "releng", "active": true}, + {"name": "initiatives", "active": true}, + ] + } + + """ + + repo = _get_repo(repo, username, namespace) + _check_issue_tracker(repo) + + boards = repo.boards + + jsonout = { + "total_requests": len(boards), + "boards": [board.to_json() for board in boards], + } + return flask.jsonify(jsonout) + + +@API.route("//boards", methods=["POST"]) +@API.route("///boards", methods=["POST"]) +@API.route("/fork///boards", methods=["POST"]) +@API.route( + "/fork////boards", methods=["POST"], +) +@api_login_required(acls=["modify_project"]) +@api_method +def api_board_create(repo, username=None, namespace=None): + """ + Create a board + -------------- + Create a new board on a project + + :: + + POST /api/0//boards + POST /api/0///boards + + :: + + POST /api/0/fork///boards + POST /api/0/fork////boards + + + Input + ^^^^^ + + { + "board_name": { + "active": , + "tag": + }, + "Infrastructure": { + "active": true, + "tag": "backlog" + } + } + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "boards": [ + { + "active": True, + "name": "dev", + "status": [], + "tag": { + "tag": "dev", + "tag_color": "DeepBlueSky", + "tag_description": "", + }, + }, + { + "active": True, + "name": "infra", + "status": [], + "tag": { + "tag": "infra", + "tag_color": "DeepGreen", + "tag_description": "", + }, + }, + ] + } + + """ # noqa + + repo = _get_repo(repo, username, namespace) + + _check_issue_tracker(repo) + _check_token(repo, project_token=False) + + data = flask.request.get_json() or {} + if not data: + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="No (JSON) data provided", + ) + for key in data: + if not isinstance(data[key], bool) and "tag" not in data[key]: + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="No tag associated with at least one of the boards", + ) + + names = list(data.keys()) + + existing_board_names = set(board.name for board in repo.boards) + removing_names = set(existing_board_names) - set(names) + + for name in data: + if name not in existing_board_names: + try: + pagure.lib.query.create_board( + flask.g.session, + project=repo, + name=name, + active=data[name].get("active", False), + tag=data[name]["tag"], + ) + flask.g.session.commit() + except pagure.exceptions.PagureException as err: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.ENOCODE, error=str(err) + ) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EDBERROR + ) + else: + try: + pagure.lib.query.edit_board( + flask.g.session, + project=repo, + name=name, + active=data[name].get("active", False), + tag=data[name]["tag"], + ) + flask.g.session.commit() + except pagure.exceptions.PagureException as err: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.ENOCODE, error=str(err) + ) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EDBERROR + ) + + if removing_names: + try: + pagure.lib.query.delete_board( + flask.g.session, project=repo, names=removing_names, + ) + flask.g.session.commit() + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + + return flask.jsonify( + {"boards": [board.to_json() for board in repo.boards]} + ) + + +@API.route("//boards/delete", methods=["POST"]) +@API.route("///boards/delete", methods=["POST"]) +@API.route("/fork///boards/delete", methods=["POST"]) +@API.route( + "/fork////boards/delete", methods=["POST"], +) +@api_login_required(acls=["modify_project"]) +@api_method +def api_board_delete(repo, username=None, namespace=None): + """ + Delete a board + --------------- + Delet a board of a project + + :: + + POST /api/0//boards/delete + POST /api/0///boards/delete + + :: + + POST /api/0/fork///boards/delete + POST /api/0/fork////boards/delete + + + Input + ^^^^^ + + +---------------------+---------+-------------+-----------------------------+ + | Key | Type | Optionality | Description | + +=====================+=========+=============+=============================+ + | ``name`` | string | Mandatory | | The name of the board to | + | | | | delete. | + +---------------------+---------+-------------+-----------------------------+ + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + } + + """ # noqa + + repo = _get_repo(repo, username, namespace) + + _check_issue_tracker(repo) + _check_token(repo, project_token=False) + + fields = get_request_data() + if not isinstance(fields, werkzeug.datastructures.ImmutableMultiDict): + names_in = fields.get("name") or [] + else: + names_in = fields.getlist("name") + + names = [] + for idx, name in enumerate(names_in): + if name.strip(): + names.append(name) + + if not names: + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors={"name": ["This field is required"]}, + ) + + try: + pagure.lib.query.delete_board( + flask.g.session, project=repo, names=names, + ) + flask.g.session.commit() + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + + repo = _get_repo(repo.name, username, namespace) + return flask.jsonify( + {"boards": [board.to_json() for board in repo.boards]} + ) + + +@API.route("//boards//status", methods=["POST"]) +@API.route("///boards//status", methods=["POST"]) +@API.route( + "/fork///boards//status", methods=["POST"] +) +@API.route( + "/fork////boards//status", + methods=["POST"], +) +@api_login_required(acls=["modify_project"]) +@api_method +def api_board_status(repo, board_name, username=None, namespace=None): + """ + Update board statuses + --------------------- + Set or update the statuses a board has. + + :: + + POST /api/0//boards + POST /api/0///boards + + :: + + POST /api/0/fork///boards + POST /api/0/fork////boards + + + Input + ^^^^^ + + Submitted as JSON (Requires setting a + ``contentType: 'application/json; charset=utf-8'`` header): + + :: + + { + "Triaged": { + "close": false, + "close_status": "", + "bg_color": "#ca0dcd", + "default": true, + "rank": 1 + }, + "In Progress": { + "close": false, + "close_status": "", + "bg_color": "#1780ec", + "default": false, + "rank": 2 + }, + "In Review": { + "close": false, + "close_status": "", + "bg_color": "#f28b20", + "default": false, + "rank": 3 + }, + "Done": { + "close": true, + "close_status": "Fixed", + "bg_color": "#34d240", + "default": false, + "rank": 4 + }, + "Blocked": { + "close": false, + "close_status": "", + "bg_color": "#ff0022", + "default": false, + "rank": 5 + } + } + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "board": { + "active": True, + "name": "dev", + "status": [ + { + "bg_color": "#FFB300", + "close": false, + "close_status": None, + "name": "Backlog", + }, + { + "bg_color": "#ca0eef", + "close": false, + "close_status": None, + "name": "In Progress", + }, + { + "name": "Done", + "close": true, + "close_status": "Fixed", + "bg_color": "#34d240", + }, + ], + "tag": { + "tag": "dev", + "tag_color": "DeepBlueSky", + "tag_description": "", + }, + } + } + + """ # noqa + + repo = _get_repo(repo, username, namespace) + + _check_issue_tracker(repo) + _check_token(repo, project_token=False) + + board = None + for board_obj in repo.boards: + if board_obj.name == board_name: + board = board_obj + break + + if board is None: + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.EINVALIDREQ, errors="Board not found", + ) + + data = flask.request.get_json() or {} + if not data: + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="No (JSON) data provided", + ) + + defaults = [] + for key in data: + if key.strip(): + if not TAGS_REGEX_RE.match(key): + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors={ + "name": [ + "Invalid status name provided, it " + "should match: %s." % TAGS_REGEX + ] + }, + ) + if ( + len( + set(data[key].keys()).intersection( + set(["rank", "default"]) + ) + ) + != 2 + ): + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="The 'rank' and 'default' fields are" " mandatory.", + ) + if data[key]["default"] is True: + defaults.append(key) + + if len(defaults) != 1: + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="There must be one and only one default.", + ) + + for status in board.statuses: + if status.name not in data: + _log.debug("Removing status: %s", status.name) + flask.g.session.delete(status) + + for name in data: + if not name.strip(): + continue + + try: + close_status = data[name].get("close_status") or None + close = data[name].get("close") or True if close_status else False + if close_status not in repo.close_status: + close_status = None + + pagure.lib.query.update_board_status( + flask.g.session, + board=board, + name=name, + rank=data[name]["rank"], + default=data[name]["default"], + bg_color=data[name].get("bg_color") or None, + close=close, + close_status=close_status, + ) + flask.g.session.commit() + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + + return flask.jsonify({"board": board.to_json()}) + + +@API.route("//boards//update_issue", methods=["POST"]) +@API.route( + "///boards//update_issue", methods=["POST"] +) +@API.route( + "/fork///boards//update_issue", + methods=["POST"], +) +@API.route( + "/fork////boards//update_issue", + methods=["POST"], +) +@api_login_required(acls=["modify_project"]) +@api_method +def api_board_ticket_update_status( + repo, board_name, username=None, namespace=None +): + """ + Update a ticket on a board + -------------------------- + Update a ticket on a board (ie: update its status). + + :: + + POST /api/0//boards/update_issue + POST /api/0///boards/update_issue + + :: + + POST /api/0/fork///boards/update_issue + POST /api/0/fork////boards/update_issue + + + Input + ^^^^^ + + Submitted as JSON (Requires setting a + ``contentType: 'application/json; charset=utf-8'`` header): + + :: + + { + "ticket_uid": { + "status": "status_name" + "rank": 1 + }, + "asdas12e1dasdasd12e12e": { + "status": "In Progress" + "rank": 2 + } + } + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + {"name": "infrastructure", "active": true}, + } + + """ # noqa + + repo = _get_repo(repo, username, namespace) + + _check_issue_tracker(repo) + _check_token(repo, project_token=False) + + board = None + for board_obj in repo.boards: + if board_obj.name == board_name: + board = board_obj + break + + if board is None: + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.EINVALIDREQ, errors="Board not found", + ) + + data = flask.request.get_json() or {} + if not data: + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="No (JSON) data provided", + ) + + for key in data: + if key.strip(): + if ( + len( + set(data[key].keys()).intersection(set(["rank", "status"])) + ) + != 2 + ): + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="The 'rank' and 'status' fields are mandatory.", + ) + + for ticket_uid in data: + if not ticket_uid.strip(): + continue + + try: + pagure.lib.query.update_ticket_board_status( + flask.g.session, + board=board, + user=flask.g.fas_user.username, + ticket_uid=ticket_uid, + rank=data[ticket_uid]["rank"], + status_name=data[ticket_uid]["status"], + ) + flask.g.session.commit() + except pagure.exceptions.PagureException as err: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.ENOCODE, error=str(err) + ) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + + return flask.jsonify({"board": board_obj.to_json()}) + + +@API.route("//boards//add_issue", methods=["POST"]) +@API.route( + "///boards//add_issue", methods=["POST"] +) +@API.route( + "/fork///boards//add_issue", methods=["POST"], +) +@API.route( + "/fork////boards//add_issue", + methods=["POST"], +) +@api_login_required(acls=["modify_project"]) +@api_method +def api_board_ticket_add_status( + repo, board_name, username=None, namespace=None +): + """ + Add a ticket on a board + -------------------------- + Add a ticket on a board (ie: update its status). + + :: + + POST /api/0//boards/update_issue + POST /api/0///boards/update_issue + + :: + + POST /api/0/fork///boards/update_issue + POST /api/0/fork////boards/update_issue + + + Input + ^^^^^ + + Submitted as JSON (Requires setting a + ``contentType: 'application/json; charset=utf-8'`` header): + + :: + + { + "ticket_id_in_the_project": { + "status": "status_name" + "rank": 1 + }, + "12": { + "status": "In Progress" + "rank": 2 + } + } + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + {"name": "infrastructure", "active": true}, + } + + """ # noqa + + repo = _get_repo(repo, username, namespace) + + _check_issue_tracker(repo) + _check_token(repo, project_token=False) + + board = None + for board_obj in repo.boards: + if board_obj.name == board_name: + board = board_obj + break + + if board is None: + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.EINVALIDREQ, errors="Board not found", + ) + + data = flask.request.get_json() or {} + if not data: + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="No (JSON) data provided", + ) + + for key in data: + if key.strip(): + if ( + len( + set(data[key].keys()).intersection(set(["rank", "status"])) + ) + != 2 + ): + raise pagure.exceptions.APIError( + 400, + error_code=APIERROR.EINVALIDREQ, + errors="The 'rank' and 'status' fields are mandatory.", + ) + + for ticket_id in data: + if not ticket_id.strip(): + continue + + try: + pagure.lib.query.update_ticket_board_status( + flask.g.session, + board=board, + user=flask.g.fas_user.username, + ticket_id=ticket_id, + rank=data[ticket_id]["rank"], + status_name=data[ticket_id]["status"], + ) + flask.g.session.commit() + except pagure.exceptions.PagureException as err: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.ENOCODE, error=str(err) + ) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + + return flask.jsonify({"board": board_obj.to_json()}) diff --git a/pagure/flask_app.py b/pagure/flask_app.py index 34eb478..ceefd5b 100644 --- a/pagure/flask_app.py +++ b/pagure/flask_app.py @@ -529,6 +529,9 @@ def end_request(exception=None): def after_request(response): """ After request callback, adjust the headers returned """ + if not hasattr(flask.g, "nonce"): + return response + csp_headers = pagure_config["CSP_HEADERS"] try: style_csp = "nonce-" + flask.g.nonce diff --git a/pagure/forms.py b/pagure/forms.py index 6daa8a8..e29b770 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -36,10 +36,11 @@ from pagure.utils import urlpattern, is_admin STRICT_REGEX = "^[a-zA-Z0-9-_]+$" # This regex is used when creating tags, there we do not want to allow ',' # as otherwise it breaks the UI. -TAGS_REGEX = "^[a-zA-Z0-9-_ .:]+$" +TAGS_REGEX = "^[a-zA-Z0-9][a-zA-Z0-9-_ .:]+$" +TAGS_REGEX_RE = re.compile(TAGS_REGEX) # In the issue page tags are sent as a comma-separated list, so in order to # allow having multiple tags in an issue, we need to allow ',' in them. -TAGS_REGEX_MULTI = "^[a-zA-Z0-9-_, .:]+$" +TAGS_REGEX_MULTI = "^[a-zA-Z0-9][a-zA-Z0-9-_, .:]+$" FALSE_VALUES = ("false", "", False, "False", 0, "0") WTF_VERSION = tuple() diff --git a/pagure/lib/git.py b/pagure/lib/git.py index 3cc30af..53ef0b6 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -680,6 +680,71 @@ def update_ticket_from_git( if msgs: messages.extend(msgs) + # Update boards + boards = json_data.get("boards") or [] + _log.debug("Loading %s boards", len(boards)) + + # Go over the boards and add them + try: + for board_issue in boards: + board = board_issue["board"] + _log.debug("Loading board: %s", board["name"]) + tag_obj = pagure.lib.query.get_colored_tag( + session, board["tag"]["tag"], issue.project.id + ) + if not tag_obj: + _log.debug("Creating tag: %s", board["tag"]["tag"]) + pagure.lib.query.new_tag( + session, + tag_name=board["tag"]["tag"], + tag_description=board["tag"]["tag_description"], + tag_color=board["tag"]["tag_color"], + project_id=issue.project.id, + ) + board_obj = None + for b in repo.boards: + if b.name == board["name"]: + board_obj = b + break + if not board_obj: + _log.debug("Creating board: %s", board["name"]) + board_obj = pagure.lib.query.create_board( + session, + project=issue.project, + name=board["name"], + active=board["active"], + tag=board["tag"]["tag"], + ) + session.flush() + for idx, status in enumerate(board["status"]): + _log.debug("Updating status: %s", status["name"]) + pagure.lib.query.update_board_status( + session, + board=board_obj, + name=status["name"], + rank=idx, + default=status["default"], + close=status["close"], + close_status=status["close_status"], + bg_color=status["bg_color"], + ) + session.flush() + + _log.debug("Updating ticket in board") + pagure.lib.query.update_ticket_board_status( + session, + board=board_obj, + user=agent, + rank=board_issue["rank"], + status_name=board_issue["status"]["name"], + ticket_uid=issue.uid, + ticket_id=None, + ) + session.flush() + except SQLAlchemyError: + _log.exception("An error occured while loading the boards") + session.rollback() + # Update assignee assignee = get_user_from_json(session, json_data, key="assignee") if assignee: diff --git a/pagure/lib/model.py b/pagure/lib/model.py index 6fa45c3..fbac954 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -19,6 +19,7 @@ import operator import re import pygit2 import os +from operator import attrgetter import six import sqlalchemy as sa @@ -1033,6 +1034,11 @@ class Project(BASE): ), } + @property + def active_boards(self): + """ Returns the list of active boards. """ + return [board for board in self.boards if board.active] + def lock(self, ltype): """ Get a SQL lock of type ltype for the current project. """ @@ -1066,6 +1072,7 @@ class Project(BASE): } if not api and not public: output["settings"] = self.settings + output["boards"] = [b.to_json() for b in self.boards] return output @@ -1472,6 +1479,15 @@ class Issue(BASE): be sorted using this attribute. """ return self.priority if self.priority else "" + @property + def boards_name(self): + """ Return the list of boards the issue is part of + """ + out = [] + for status_board in self.boards_issues: + out.append(status_board.board.name) + return out + def to_json(self, public=False, with_comments=True, with_project=False): """ Returns a dictionary representation of the issue. @@ -1526,6 +1542,11 @@ class Issue(BASE): if with_project: output["project"] = self.project.to_json(public=public, api=True) + if self.boards_issues: + output["boards"] = [ + board_issue.to_json() for board_issue in self.boards_issues + ] + return output @@ -3130,6 +3151,239 @@ class TokenAcl(BASE): __table_args__ = (sa.UniqueConstraint("token_id", "acl_id"),) +# +# Class and tables specific for the project's boards +# + + +class Board(BASE): + """ + Table listing the boards of a project + """ + + __tablename__ = "boards" + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) + project_id = sa.Column( + sa.Integer, + sa.ForeignKey("projects.id", onupdate="CASCADE", ondelete="CASCADE"), + nullable=False, + index=True, + ) + name = sa.Column(sa.Text(), nullable=False) + active = sa.Column(sa.Boolean, nullable=False, default=True) + tag_id = sa.Column( + sa.Integer, + sa.ForeignKey("tags_colored.id", onupdate="CASCADE"), + nullable=False, + ) + + created = sa.Column( + sa.DateTime, nullable=False, default=datetime.datetime.utcnow + ) + date_updated = sa.Column( + sa.DateTime, + nullable=False, + default=datetime.datetime.utcnow, + onupdate=datetime.datetime.utcnow, + ) + + __table_args__ = ( + sa.UniqueConstraint( + "project_id", "name", name="boards_project_id_name_uix" + ), + ) + + project = relation( + "Project", + backref=backref("boards", cascade="delete, delete-orphan"), + foreign_keys=[project_id], + remote_side=[Project.id], + ) + tag = relation( + "TagColored", foreign_keys=[tag_id], remote_side=[TagColored.id] + ) + + boards_issues = relation( + "BoardIssues", + secondary="board_statuses", + primaryjoin="boards.c.id==board_statuses.c.board_id", + secondaryjoin="board_statuses.c.id==boards_issues.c.status_id", + ) + + @property + def default_status(self): + """ Returns the default status of the board. """ + out = None + for status in self.statuses: + if status.default: + out = status + break + return out + + def __repr__(self): + """ Return a string representation of this object. """ + + return "Board: %s - name %s" % (self.id, self.name,) + + def to_json(self): + """ The JSON representation of a board. """ + return { + "name": self.name, + "active": self.active, + "status": [status.to_json() for status in self.statuses], + "tag": self.tag.to_json(), + } + + +class BoardStatus(BASE): + """ + Table listing the status a board can have + """ + + __tablename__ = "board_statuses" + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) + board_id = sa.Column( + sa.Integer, + sa.ForeignKey("boards.id", onupdate="CASCADE", ondelete="CASCADE"), + nullable=False, + index=True, + ) + name = sa.Column(sa.String(128), nullable=False) + rank = sa.Column(sa.Integer, nullable=False) + default = sa.Column(sa.Boolean, nullable=False, default=False) + bg_color = sa.Column(sa.String(32), nullable=False) + close = sa.Column(sa.Boolean, nullable=False, default=False) + close_status = sa.Column(sa.Text, nullable=True) + created = sa.Column( + sa.DateTime, nullable=False, default=datetime.datetime.utcnow + ) + + __table_args__ = ( + sa.UniqueConstraint( + "board_id", "name", name="board_statuses_board_id_name_uix" + ), + ) + + board = relation( + "Board", + backref=backref( + "statuses", + cascade="delete, delete-orphan", + order_by=str("(board_statuses.c.rank).asc()"), + ), + foreign_keys=[board_id], + remote_side=[Board.id], + ) + + def __repr__(self): + """ Return a string representation of this object. """ + + return "BoardStatus: %s - board: %s - name %s" % ( + self.id, + self.board_id, + self.name, + ) + + def visible_tickets(self, watching_user): + """ Returns the sorted list of tickets visible to the user currently + watching. + + If the user currently watching (ie: ``watching_user``) is False, do not + show any of the private tickets. + If it is None, show all the tickets regardless of their private status. + Otherwise, only show the private tickets if the user watching is the + user who created it. + """ + return [ + bissue + for bissue in sorted(self.boards_issues, key=attrgetter("rank")) + if ( + not bissue.issue.private + or ( + watching_user is None + or bissue.issue.user.username == watching_user + ) + ) + ] + + def to_json(self): + """ The JSON representation of these objects. """ + return { + "name": self.name, + "bg_color": self.bg_color, + "close": self.close, + "close_status": self.close_status, + "default": self.default, + } + + +class BoardIssues(BASE): + """ + Association table linking the boards to the issues + """ + + __tablename__ = "boards_issues" + + issue_uid = sa.Column( + sa.String(32), + sa.ForeignKey("issues.uid", ondelete="CASCADE", onupdate="CASCADE"), + primary_key=True, + ) + status_id = sa.Column( + sa.Integer, + sa.ForeignKey( + "board_statuses.id", onupdate="CASCADE", ondelete="CASCADE" + ), + primary_key=True, + ) + rank = sa.Column(sa.Integer, nullable=False) + created = sa.Column( + sa.DateTime, nullable=False, default=datetime.datetime.utcnow + ) + + # Constraints + __table_args__ = ( + sa.UniqueConstraint( + "status_id", + "issue_uid", + name="boards_issues_status_id_issue_uid_uix", + ), + ) + + issue = relation( + "Issue", + backref=backref("boards_issues", cascade="delete, delete-orphan"), + foreign_keys=[issue_uid], + remote_side=[Issue.uid], + ) + + status = relation( + "BoardStatus", + backref=backref("boards_issues", cascade="delete, delete-orphan"), + foreign_keys=[status_id], + remote_side=[BoardStatus.id], + ) + + board = relation( + "Board", + secondary="board_statuses", + primaryjoin="boards_issues.c.status_id==board_statuses.c.id", + secondaryjoin="board_statuses.c.board_id==boards.c.id", + ) + + def to_json(self): + """ The JSON representation of these objects. """ + return { + "board": [bs.to_json() for bs in self.board][0] + if self.board + else [], + "status": self.status.to_json(), + "rank": self.rank, + } + + # ########################################################## # These classes are only used if you're using the `local` # authentication method diff --git a/pagure/lib/query.py b/pagure/lib/query.py index 8dc0439..f2e0e91 100644 --- a/pagure/lib/query.py +++ b/pagure/lib/query.py @@ -5858,3 +5858,320 @@ def remove_user_of_project(session, user, project, agent): ) return "User removed" + + +def create_board(session, project, name, active, tag): + """ Create a board on a given project. + + :arg session: the session with which to connect to the database. + :arg project: the model.Project of the project that is creating the + board. + :arg name: the name of the board to create. + :arg active: a boolean specifying if the board is active or not. + :arg tag: the name of the tag associated with this board. + + """ + tag_obj = get_colored_tag(session=session, tag=tag, project_id=project.id) + if not tag_obj: + raise pagure.exceptions.PagureException( + "No tag found with the name %s" % tag + ) + + board = model.Board( + project_id=project.id, name=name, active=active, tag_id=tag_obj.id + ) + session.add(board) + + return board + + +def edit_board(session, project, name, active, tag, bg_color=None): + """ Edit an existing board on a given project. + + :arg session: the session with which to connect to the database. + :arg project: the model.Project of the project that is creating the + board. + :arg name: the name of the board to create. + :arg active: a boolean specifying if the board is active or not. + :arg tag: the name of the tag associated with this board. + + """ + tag_obj = get_colored_tag(session=session, tag=tag, project_id=project.id) + if not tag_obj: + raise pagure.exceptions.PagureException( + "No tag found with the name %s" % tag + ) + + board_obj = None + for board in project.boards: + if board.name == name: + board_obj = board + break + + if not board_obj: + raise pagure.exceptions.PagureException( + 'Could not find the board "%s"' % name + ) + + board.active = active + board.tag_id = tag_obj.id + if bg_color: + board.bg_colar = bg_color + + session.add(board) + + return board + + +def delete_board(session, project, names): + """ Delete boards of a given project. + + :arg session: the session with which to connect to the database. + :arg project: the model.Project of the project that is creating the + board. + :arg names: a list of the name of the boards to remove. + + """ + for name in names: + board_obj = None + for board in project.boards: + if board.name == name: + board_obj = board + break + + if board_obj: + session.delete(board_obj) + + +def update_board_status( + session, board, name, rank, default, close, close_status, bg_color +): + """ Create or update the board statuses of a project. + + :arg session: the session with which to connect to the database. + :arg board: the model.Board of the board being updated. + :arg name: the name of the status. + :arg rank: the position of the status on the board, the lower the value, + the more to the left the status. + :arg default: whether tickets are added to this status by default. + :arg close: boolean indicating if ticket reaching this status should + be closed or not + :arg close_status: the close_status ticket reaching this status should + be close as. + :arg bg_color: the background color of the status on the board. + + """ + + status = ( + session.query(model.BoardStatus) + .filter(model.BoardStatus.board_id == board.id) + .filter(model.BoardStatus.name == name) + .first() + ) + if status: + status.default = default + status.rank = rank + status.bg_color = bg_color + status.close = close + status.close_status = close_status + else: + status = model.BoardStatus( + board_id=board.id, + name=name, + rank=rank, + default=default, + bg_color=bg_color, + close=close, + close_status=close_status, + ) + session.add(status) + + return status + + +def add_issue_to_boards( + session, issue, board_name, user, status_id=None, rank=None +): + """ Add the given issue to the boards specified. + + :arg session: the session with which to connect to the database. + :arg issue: the model.Issue of the issue to add to the boards. + :arg board_names: a list of board name to which the issue should be added. + :arg user: the username of the user performing the action + :kwarg rank: the rank of the issue on the status. + :kwarg rank: the rank of the issue on the status. + + """ + get_user(session, user) + + board_obj = None + for board in issue.project.boards: + if board.name == board_name: + board_obj = board + break + + if not board_obj: + _log.info( + "Could not add issue %s to %s : board not found", issue, board_name + ) + raise pagure.exceptions.PagureException("Board not found") + + status = ( + session.query(model.BoardStatus) + .filter(model.BoardStatus.id == status_id) + .first() + ) + if not status: + status = board.default_status + + if not status: + raise pagure.exceptions.PagureException( + "No status provided or default status found" + ) + + if rank is None: + rank = len(status.boards_issues) + 1 + + board_issue = model.BoardIssues( + issue_uid=issue.uid, status_id=status.id, rank=rank, + ) + session.add(board_issue) + + +def remove_issue_from_boards(session, issue, board_names, user): + """ Remove the given issue from the specified boards. + + :arg session: the session with which to connect to the database. + :arg issue: the model.Issue of the issue to add to the boards. + :arg board_names: a list of board name to which the issue should be added. + :arg user: the username of the user performing the action + + """ + get_user(session, user) + + for board_issue in issue.boards_issues: + if board_issue.board.active and board_issue.board.name in board_names: + _log.debug("Removing %s from %s", board_issue, issue) + session.delete(board_issue) + + +def update_ticket_board_status( + session, + board, + user, + rank=None, + status_name=None, + ticket_uid=None, + ticket_id=None, +): + """ Set the status of a ticket on a given board. + """ + if not ticket_uid and not ticket_id: + raise pagure.exceptions.PagureException( + "One of ticket_id/ticket_uid must be provided" + ) + + if ticket_uid: + _log.debug( + "Looking to add ticket %s to board %s with status %s (rank %s)", + ticket_uid, + board.name, + status_name, + rank, + ) + ticket = get_issue_by_uid(session, ticket_uid) + else: + _log.debug( + "Looking to add ticket %s to board %s with status %s (rank %s)", + ticket_id, + board.name, + status_name, + rank, + ) + ticket = search_issues(session, board.project, issueid=ticket_id) + + if not ticket: + raise pagure.exceptions.PagureException( + "No ticket found with this identifier" + ) + + _log.debug("Ticket found") + + status = ( + session.query(model.BoardStatus) + .filter(model.BoardStatus.board_id == board.id) + .filter(model.BoardStatus.name == status_name) + .first() + ) + if not status: + _log.debug( + "No status found with %s, using the default status" % status_name + ) + status = board.default_status + + if not status: + raise pagure.exceptions.PagureException( + "No status provided or default status found" + ) + + if rank is None: + rank = len(status.boards_issues) + 1 + + comments = [] + seen = [] + if ticket.boards_issues: + for board_issue in ticket.boards_issues: + if board_issue.status.board.name == board.name: + _log.debug("Updating existing board") + board_issue.status_id = status.id + board_issue.rank = rank + session.add(board_issue) + seen.append(board.name) + break + + if board.name not in seen: + _log.debug("Adding to a new board") + board_issue = model.BoardIssues( + issue_uid=ticket.uid, status_id=status.id, rank=rank, + ) + session.add(board_issue) + + # Flag the ticket if needed + if board.tag.tag not in ticket.tags_text: + add_tag_obj(session, obj=ticket, tags=board.tag.tag, user=user) + comments.append( + "%s tagged with: %s" % (ticket.isa.capitalize(), board.tag.tag) + ) + + if status.close and ticket.status == "Open": + _log.debug( + "Closing ticket %s as the new status on the board is " + "set to close tickets", + ticket, + ) + comments.extend( + edit_issue( + session=session, + issue=ticket, + user=user, + status="Closed", + close_status=status.close_status or None, + ) + ) + session.add(ticket) + elif not status.close and ticket.status != "Open": + comments.extend( + edit_issue( + session=session, issue=ticket, user=user, status="Open", + ) + ) + session.add(ticket) + + if comments: + add_issue_comment( + session=session, + issue=ticket, + comment="\n".join(comments), + user=user, + notification=True, + ) diff --git a/pagure/static/pagure_boards.css b/pagure/static/pagure_boards.css new file mode 100644 index 0000000..46496ff --- /dev/null +++ b/pagure/static/pagure_boards.css @@ -0,0 +1,137 @@ +ul { + list-style-type: none; + margin: 0; + padding: 0; +} + +.drag-container { + margin: 20px auto; +} + +.drag-list { + display: -webkit-box; + display: flex; + -webkit-box-align: start; + align-items: flex-start; +} + +@media (max-width: 690px) { + .drag-list { + display: block; + } +} + +.drag-column { + border-radius: 10px; + -webkit-box-flex: 1; + flex: 1; + margin: 0 10px; + position: relative; + background: rgba(0, 0, 0, 0.2); + overflow: hidden; +} +@media (max-width: 690px) { + .drag-column { + margin-bottom: 30px; + } +} + +.drag-column h2 { + font-size: 0.8rem; + margin: 0; + text-transform: uppercase; + font-weight: 600; +} + +.drag-column-header { + display: -webkit-box; + display: flex; + -webkit-box-align: center; + align-items: center; + -webkit-box-pack: justify; + justify-content: space-between; + padding: 10px; +} + +.drag-inner-list { + min-height: 250px; +} + +.drag-item { + border-radius: 8px; + margin: 10px; + height: 100px; + background: rgba(0, 0, 0, 0.5); + -webkit-transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); + color: white; + padding: 4px 8px; +} +.drag-item.is-moving { + -webkit-transform: scale(1.25); + transform: scale(1.25); + background: rgba(0, 0, 0, 0.8); +} +.drag-item a { + color: white; +} + +.drag-header-more { + cursor: pointer; +} + +.drag-options { + position: absolute; + top: 44px; + left: 0; + width: 100%; + height: 100%; + padding: 10px; + -webkit-transform: translateX(100%); + transform: translateX(100%); + opacity: 0; + -webkit-transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); + transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); +} +.drag-options.active { + -webkit-transform: translateX(0); + transform: translateX(0); + opacity: 1; +} +.drag-options-label { + display: block; + margin: 0 0 5px 0; +} +.drag-options-label input { + opacity: 0.6; +} +.drag-options-label span { + display: inline-block; + font-size: 0.9rem; + font-weight: 400; + margin-left: 5px; +} + +/* Dragula CSS */ +.gu-mirror { + position: fixed !important; + margin: 0 !important; + z-index: 9999 !important; + opacity: 0.8; + list-style-type: none; +} + +.gu-hide { + display: none !important; +} + +.gu-unselectable { + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; +} + +.gu-transit { + opacity: 0.2; +} diff --git a/pagure/static/tags.js b/pagure/static/tags.js index b6bab2b..90aef1a 100644 --- a/pagure/static/tags.js +++ b/pagure/static/tags.js @@ -2,4 +2,4 @@ $(document).ready(function() { $("[data-bg-color").each(function(ind, obj) { $(obj).css('background-color', $(obj).attr('data-bg-color')); }); -}); \ No newline at end of file +}); diff --git a/pagure/static/vendor/dragula/dragula-3.7.2.js b/pagure/static/vendor/dragula/dragula-3.7.2.js new file mode 100644 index 0000000..67b9381 --- /dev/null +++ b/pagure/static/vendor/dragula/dragula-3.7.2.js @@ -0,0 +1,908 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.dragula = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ? revert : o.revertOnSpill; + var item = _copy || _item; + var parent = getParent(item); + var initial = isInitialPlacement(parent); + if (initial === false && reverts) { + if (_copy) { + if (parent) { + parent.removeChild(_copy); + } + } else { + _source.insertBefore(item, _initialSibling); + } + } + if (initial || reverts) { + drake.emit('cancel', item, _source, _source); + } else { + drake.emit('drop', item, parent, _source, _currentSibling); + } + cleanup(); + } + + function cleanup () { + var item = _copy || _item; + ungrab(); + removeMirrorImage(); + if (item) { + classes.rm(item, 'gu-transit'); + } + if (_renderTimer) { + clearTimeout(_renderTimer); + } + drake.dragging = false; + if (_lastDropTarget) { + drake.emit('out', item, _lastDropTarget, _source); + } + drake.emit('dragend', item); + _source = _item = _copy = _initialSibling = _currentSibling = _renderTimer = _lastDropTarget = null; + } + + function isInitialPlacement (target, s) { + var sibling; + if (s !== void 0) { + sibling = s; + } else if (_mirror) { + sibling = _currentSibling; + } else { + sibling = nextEl(_copy || _item); + } + return target === _source && sibling === _initialSibling; + } + + function findDropTarget (elementBehindCursor, clientX, clientY) { + var target = elementBehindCursor; + while (target && !accepted()) { + target = getParent(target); + } + return target; + + function accepted () { + var droppable = isContainer(target); + if (droppable === false) { + return false; + } + + var immediate = getImmediateChild(target, elementBehindCursor); + var reference = getReference(target, immediate, clientX, clientY); + var initial = isInitialPlacement(target, reference); + if (initial) { + return true; // should always be able to drop it right back where it was + } + return o.accepts(_item, target, _source, reference); + } + } + + function drag (e) { + if (!_mirror) { + return; + } + e.preventDefault(); + + var clientX = getCoord('clientX', e); + var clientY = getCoord('clientY', e); + var x = clientX - _offsetX; + var y = clientY - _offsetY; + + _mirror.style.left = x + 'px'; + _mirror.style.top = y + 'px'; + + var item = _copy || _item; + var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY); + var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY); + var changed = dropTarget !== null && dropTarget !== _lastDropTarget; + if (changed || dropTarget === null) { + out(); + _lastDropTarget = dropTarget; + over(); + } + var parent = getParent(item); + if (dropTarget === _source && _copy && !o.copySortSource) { + if (parent) { + parent.removeChild(item); + } + return; + } + var reference; + var immediate = getImmediateChild(dropTarget, elementBehindCursor); + if (immediate !== null) { + reference = getReference(dropTarget, immediate, clientX, clientY); + } else if (o.revertOnSpill === true && !_copy) { + reference = _initialSibling; + dropTarget = _source; + } else { + if (_copy && parent) { + parent.removeChild(item); + } + return; + } + if ( + (reference === null && changed) || + reference !== item && + reference !== nextEl(item) + ) { + _currentSibling = reference; + dropTarget.insertBefore(item, reference); + drake.emit('shadow', item, dropTarget, _source); + } + function moved (type) { drake.emit(type, item, _lastDropTarget, _source); } + function over () { if (changed) { moved('over'); } } + function out () { if (_lastDropTarget) { moved('out'); } } + } + + function spillOver (el) { + classes.rm(el, 'gu-hide'); + } + + function spillOut (el) { + if (drake.dragging) { classes.add(el, 'gu-hide'); } + } + + function renderMirrorImage () { + if (_mirror) { + return; + } + var rect = _item.getBoundingClientRect(); + _mirror = _item.cloneNode(true); + _mirror.style.width = getRectWidth(rect) + 'px'; + _mirror.style.height = getRectHeight(rect) + 'px'; + classes.rm(_mirror, 'gu-transit'); + classes.add(_mirror, 'gu-mirror'); + o.mirrorContainer.appendChild(_mirror); + touchy(documentElement, 'add', 'mousemove', drag); + classes.add(o.mirrorContainer, 'gu-unselectable'); + drake.emit('cloned', _mirror, _item, 'mirror'); + } + + function removeMirrorImage () { + if (_mirror) { + classes.rm(o.mirrorContainer, 'gu-unselectable'); + touchy(documentElement, 'remove', 'mousemove', drag); + getParent(_mirror).removeChild(_mirror); + _mirror = null; + } + } + + function getImmediateChild (dropTarget, target) { + var immediate = target; + while (immediate !== dropTarget && getParent(immediate) !== dropTarget) { + immediate = getParent(immediate); + } + if (immediate === documentElement) { + return null; + } + return immediate; + } + + function getReference (dropTarget, target, x, y) { + var horizontal = o.direction === 'horizontal'; + var reference = target !== dropTarget ? inside() : outside(); + return reference; + + function outside () { // slower, but able to figure out any position + var len = dropTarget.children.length; + var i; + var el; + var rect; + for (i = 0; i < len; i++) { + el = dropTarget.children[i]; + rect = el.getBoundingClientRect(); + if (horizontal && (rect.left + rect.width / 2) > x) { return el; } + if (!horizontal && (rect.top + rect.height / 2) > y) { return el; } + } + return null; + } + + function inside () { // faster, but only available if dropped inside a child element + var rect = target.getBoundingClientRect(); + if (horizontal) { + return resolve(x > rect.left + getRectWidth(rect) / 2); + } + return resolve(y > rect.top + getRectHeight(rect) / 2); + } + + function resolve (after) { + return after ? nextEl(target) : target; + } + } + + function isCopy (item, container) { + return typeof o.copy === 'boolean' ? o.copy : o.copy(item, container); + } +} + +function touchy (el, op, type, fn) { + var touch = { + mouseup: 'touchend', + mousedown: 'touchstart', + mousemove: 'touchmove' + }; + var pointers = { + mouseup: 'pointerup', + mousedown: 'pointerdown', + mousemove: 'pointermove' + }; + var microsoft = { + mouseup: 'MSPointerUp', + mousedown: 'MSPointerDown', + mousemove: 'MSPointerMove' + }; + if (global.navigator.pointerEnabled) { + crossvent[op](el, pointers[type], fn); + } else if (global.navigator.msPointerEnabled) { + crossvent[op](el, microsoft[type], fn); + } else { + crossvent[op](el, touch[type], fn); + crossvent[op](el, type, fn); + } +} + +function whichMouseButton (e) { + if (e.touches !== void 0) { return e.touches.length; } + if (e.which !== void 0 && e.which !== 0) { return e.which; } // see https://github.com/bevacqua/dragula/issues/261 + if (e.buttons !== void 0) { return e.buttons; } + var button = e.button; + if (button !== void 0) { // see https://github.com/jquery/jquery/blob/99e8ff1baa7ae341e94bb89c3e84570c7c3ad9ea/src/event.js#L573-L575 + return button & 1 ? 1 : button & 2 ? 3 : (button & 4 ? 2 : 0); + } +} + +function getOffset (el) { + var rect = el.getBoundingClientRect(); + return { + left: rect.left + getScroll('scrollLeft', 'pageXOffset'), + top: rect.top + getScroll('scrollTop', 'pageYOffset') + }; +} + +function getScroll (scrollProp, offsetProp) { + if (typeof global[offsetProp] !== 'undefined') { + return global[offsetProp]; + } + if (documentElement.clientHeight) { + return documentElement[scrollProp]; + } + return doc.body[scrollProp]; +} + +function getElementBehindPoint (point, x, y) { + var p = point || {}; + var state = p.className; + var el; + p.className += ' gu-hide'; + el = doc.elementFromPoint(x, y); + p.className = state; + return el; +} + +function never () { return false; } +function always () { return true; } +function getRectWidth (rect) { return rect.width || (rect.right - rect.left); } +function getRectHeight (rect) { return rect.height || (rect.bottom - rect.top); } +function getParent (el) { return el.parentNode === doc ? null : el.parentNode; } +function isInput (el) { return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || isEditable(el); } +function isEditable (el) { + if (!el) { return false; } // no parents were editable + if (el.contentEditable === 'false') { return false; } // stop the lookup + if (el.contentEditable === 'true') { return true; } // found a contentEditable element in the chain + return isEditable(getParent(el)); // contentEditable is set to 'inherit' +} + +function nextEl (el) { + return el.nextElementSibling || manually(); + function manually () { + var sibling = el; + do { + sibling = sibling.nextSibling; + } while (sibling && sibling.nodeType !== 1); + return sibling; + } +} + +function getEventHost (e) { + // on touchend event, we have to use `e.changedTouches` + // see http://stackoverflow.com/questions/7192563/touchend-event-properties + // see https://github.com/bevacqua/dragula/issues/34 + if (e.targetTouches && e.targetTouches.length) { + return e.targetTouches[0]; + } + if (e.changedTouches && e.changedTouches.length) { + return e.changedTouches[0]; + } + return e; +} + +function getCoord (coord, e) { + var host = getEventHost(e); + var missMap = { + pageX: 'clientX', // IE8 + pageY: 'clientY' // IE8 + }; + if (coord in missMap && !(coord in host) && missMap[coord] in host) { + coord = missMap[coord]; + } + return host[coord]; +} + +module.exports = dragula; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./classes":1,"contra/emitter":5,"crossvent":6}],3:[function(require,module,exports){ +module.exports = function atoa (a, n) { return Array.prototype.slice.call(a, n); } + +},{}],4:[function(require,module,exports){ +'use strict'; + +var ticky = require('ticky'); + +module.exports = function debounce (fn, args, ctx) { + if (!fn) { return; } + ticky(function run () { + fn.apply(ctx || null, args || []); + }); +}; + +},{"ticky":9}],5:[function(require,module,exports){ +'use strict'; + +var atoa = require('atoa'); +var debounce = require('./debounce'); + +module.exports = function emitter (thing, options) { + var opts = options || {}; + var evt = {}; + if (thing === undefined) { thing = {}; } + thing.on = function (type, fn) { + if (!evt[type]) { + evt[type] = [fn]; + } else { + evt[type].push(fn); + } + return thing; + }; + thing.once = function (type, fn) { + fn._once = true; // thing.off(fn) still works! + thing.on(type, fn); + return thing; + }; + thing.off = function (type, fn) { + var c = arguments.length; + if (c === 1) { + delete evt[type]; + } else if (c === 0) { + evt = {}; + } else { + var et = evt[type]; + if (!et) { return thing; } + et.splice(et.indexOf(fn), 1); + } + return thing; + }; + thing.emit = function () { + var args = atoa(arguments); + return thing.emitterSnapshot(args.shift()).apply(this, args); + }; + thing.emitterSnapshot = function (type) { + var et = (evt[type] || []).slice(0); + return function () { + var args = atoa(arguments); + var ctx = this || thing; + if (type === 'error' && opts.throws !== false && !et.length) { throw args.length === 1 ? args[0] : args; } + et.forEach(function emitter (listen) { + if (opts.async) { debounce(listen, args, ctx); } else { listen.apply(ctx, args); } + if (listen._once) { thing.off(type, listen); } + }); + return thing; + }; + }; + return thing; +}; + +},{"./debounce":4,"atoa":3}],6:[function(require,module,exports){ +(function (global){ +'use strict'; + +var customEvent = require('custom-event'); +var eventmap = require('./eventmap'); +var doc = global.document; +var addEvent = addEventEasy; +var removeEvent = removeEventEasy; +var hardCache = []; + +if (!global.addEventListener) { + addEvent = addEventHard; + removeEvent = removeEventHard; +} + +module.exports = { + add: addEvent, + remove: removeEvent, + fabricate: fabricateEvent +}; + +function addEventEasy (el, type, fn, capturing) { + return el.addEventListener(type, fn, capturing); +} + +function addEventHard (el, type, fn) { + return el.attachEvent('on' + type, wrap(el, type, fn)); +} + +function removeEventEasy (el, type, fn, capturing) { + return el.removeEventListener(type, fn, capturing); +} + +function removeEventHard (el, type, fn) { + var listener = unwrap(el, type, fn); + if (listener) { + return el.detachEvent('on' + type, listener); + } +} + +function fabricateEvent (el, type, model) { + var e = eventmap.indexOf(type) === -1 ? makeCustomEvent() : makeClassicEvent(); + if (el.dispatchEvent) { + el.dispatchEvent(e); + } else { + el.fireEvent('on' + type, e); + } + function makeClassicEvent () { + var e; + if (doc.createEvent) { + e = doc.createEvent('Event'); + e.initEvent(type, true, true); + } else if (doc.createEventObject) { + e = doc.createEventObject(); + } + return e; + } + function makeCustomEvent () { + return new customEvent(type, { detail: model }); + } +} + +function wrapperFactory (el, type, fn) { + return function wrapper (originalEvent) { + var e = originalEvent || global.event; + e.target = e.target || e.srcElement; + e.preventDefault = e.preventDefault || function preventDefault () { e.returnValue = false; }; + e.stopPropagation = e.stopPropagation || function stopPropagation () { e.cancelBubble = true; }; + e.which = e.which || e.keyCode; + fn.call(el, e); + }; +} + +function wrap (el, type, fn) { + var wrapper = unwrap(el, type, fn) || wrapperFactory(el, type, fn); + hardCache.push({ + wrapper: wrapper, + element: el, + type: type, + fn: fn + }); + return wrapper; +} + +function unwrap (el, type, fn) { + var i = find(el, type, fn); + if (i) { + var wrapper = hardCache[i].wrapper; + hardCache.splice(i, 1); // free up a tad of memory + return wrapper; + } +} + +function find (el, type, fn) { + var i, item; + for (i = 0; i < hardCache.length; i++) { + item = hardCache[i]; + if (item.element === el && item.type === type && item.fn === fn) { + return i; + } + } +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{"./eventmap":7,"custom-event":8}],7:[function(require,module,exports){ +(function (global){ +'use strict'; + +var eventmap = []; +var eventname = ''; +var ron = /^on/; + +for (eventname in global) { + if (ron.test(eventname)) { + eventmap.push(eventname.slice(2)); + } +} + +module.exports = eventmap; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],8:[function(require,module,exports){ +(function (global){ + +var NativeCustomEvent = global.CustomEvent; + +function useNative () { + try { + var p = new NativeCustomEvent('cat', { detail: { foo: 'bar' } }); + return 'cat' === p.type && 'bar' === p.detail.foo; + } catch (e) { + } + return false; +} + +/** + * Cross-browser `CustomEvent` constructor. + * + * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent.CustomEvent + * + * @public + */ + +module.exports = useNative() ? NativeCustomEvent : + +// IE >= 9 +'function' === typeof document.createEvent ? function CustomEvent (type, params) { + var e = document.createEvent('CustomEvent'); + if (params) { + e.initCustomEvent(type, params.bubbles, params.cancelable, params.detail); + } else { + e.initCustomEvent(type, false, false, void 0); + } + return e; +} : + +// IE <= 8 +function CustomEvent (type, params) { + var e = document.createEventObject(); + e.type = type; + if (params) { + e.bubbles = Boolean(params.bubbles); + e.cancelable = Boolean(params.cancelable); + e.detail = params.detail; + } else { + e.bubbles = false; + e.cancelable = false; + e.detail = void 0; + } + return e; +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],9:[function(require,module,exports){ +var si = typeof setImmediate === 'function', tick; +if (si) { + tick = function (fn) { setImmediate(fn); }; +} else { + tick = function (fn) { setTimeout(fn, 0); }; +} + +module.exports = tick; +},{}]},{},[2])(2) +}); +//# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJjbGFzc2VzLmpzIiwiZHJhZ3VsYS5qcyIsIm5vZGVfbW9kdWxlcy9hdG9hL2F0b2EuanMiLCJub2RlX21vZHVsZXMvY29udHJhL2RlYm91bmNlLmpzIiwibm9kZV9tb2R1bGVzL2NvbnRyYS9lbWl0dGVyLmpzIiwibm9kZV9tb2R1bGVzL2Nyb3NzdmVudC9zcmMvY3Jvc3N2ZW50LmpzIiwibm9kZV9tb2R1bGVzL2Nyb3NzdmVudC9zcmMvZXZlbnRtYXAuanMiLCJub2RlX21vZHVsZXMvY3VzdG9tLWV2ZW50L2luZGV4LmpzIiwibm9kZV9tb2R1bGVzL3RpY2t5L3RpY2t5LWJyb3dzZXIuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7O0FDakNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7OztBQ2htQkE7QUFDQTs7QUNEQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ1ZBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOzs7QUN0REE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOzs7OztBQ3JHQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOzs7OztBQ2JBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOzs7O0FDaERBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJmaWxlIjoiZ2VuZXJhdGVkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXNDb250ZW50IjpbIihmdW5jdGlvbiBlKHQsbixyKXtmdW5jdGlvbiBzKG8sdSl7aWYoIW5bb10pe2lmKCF0W29dKXt2YXIgYT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2lmKCF1JiZhKXJldHVybiBhKG8sITApO2lmKGkpcmV0dXJuIGkobywhMCk7dmFyIGY9bmV3IEVycm9yKFwiQ2Fubm90IGZpbmQgbW9kdWxlICdcIitvK1wiJ1wiKTt0aHJvdyBmLmNvZGU9XCJNT0RVTEVfTk9UX0ZPVU5EXCIsZn12YXIgbD1uW29dPXtleHBvcnRzOnt9fTt0W29dWzBdLmNhbGwobC5leHBvcnRzLGZ1bmN0aW9uKGUpe3ZhciBuPXRbb11bMV1bZV07cmV0dXJuIHMobj9uOmUpfSxsLGwuZXhwb3J0cyxlLHQsbixyKX1yZXR1cm4gbltvXS5leHBvcnRzfXZhciBpPXR5cGVvZiByZXF1aXJlPT1cImZ1bmN0aW9uXCImJnJlcXVpcmU7Zm9yKHZhciBvPTA7bzxyLmxlbmd0aDtvKyspcyhyW29dKTtyZXR1cm4gc30pIiwiJ3VzZSBzdHJpY3QnO1xuXG52YXIgY2FjaGUgPSB7fTtcbnZhciBzdGFydCA9ICcoPzpefFxcXFxzKSc7XG52YXIgZW5kID0gJyg/OlxcXFxzfCQpJztcblxuZnVuY3Rpb24gbG9va3VwQ2xhc3MgKGNsYXNzTmFtZSkge1xuICB2YXIgY2FjaGVkID0gY2FjaGVbY2xhc3NOYW1lXTtcbiAgaWYgKGNhY2hlZCkge1xuICAgIGNhY2hlZC5sYXN0SW5kZXggPSAwO1xuICB9IGVsc2Uge1xuICAgIGNhY2hlW2NsYXNzTmFtZV0gPSBjYWNoZWQgPSBuZXcgUmVnRXhwKHN0YXJ0ICsgY2xhc3NOYW1lICsgZW5kLCAnZycpO1xuICB9XG4gIHJldHVybiBjYWNoZWQ7XG59XG5cbmZ1bmN0aW9uIGFkZENsYXNzIChlbCwgY2xhc3NOYW1lKSB7XG4gIHZhciBjdXJyZW50ID0gZWwuY2xhc3NOYW1lO1xuICBpZiAoIWN1cnJlbnQubGVuZ3RoKSB7XG4gICAgZWwuY2xhc3NOYW1lID0gY2xhc3NOYW1lO1xuICB9IGVsc2UgaWYgKCFsb29rdXBDbGFzcyhjbGFzc05hbWUpLnRlc3QoY3VycmVudCkpIHtcbiAgICBlbC5jbGFzc05hbWUgKz0gJyAnICsgY2xhc3NOYW1lO1xuICB9XG59XG5cbmZ1bmN0aW9uIHJtQ2xhc3MgKGVsLCBjbGFzc05hbWUpIHtcbiAgZWwuY2xhc3NOYW1lID0gZWwuY2xhc3NOYW1lLnJlcGxhY2UobG9va3VwQ2xhc3MoY2xhc3NOYW1lKSwgJyAnKS50cmltKCk7XG59XG5cbm1vZHVsZS5leHBvcnRzID0ge1xuICBhZGQ6IGFkZENsYXNzLFxuICBybTogcm1DbGFzc1xufTtcbiIsIid1c2Ugc3RyaWN0JztcblxudmFyIGVtaXR0ZXIgPSByZXF1aXJlKCdjb250cmEvZW1pdHRlcicpO1xudmFyIGNyb3NzdmVudCA9IHJlcXVpcmUoJ2Nyb3NzdmVudCcpO1xudmFyIGNsYXNzZXMgPSByZXF1aXJlKCcuL2NsYXNzZXMnKTtcbnZhciBkb2MgPSBkb2N1bWVudDtcbnZhciBkb2N1bWVudEVsZW1lbnQgPSBkb2MuZG9jdW1lbnRFbGVtZW50O1xuXG5mdW5jdGlvbiBkcmFndWxhIChpbml0aWFsQ29udGFpbmVycywgb3B0aW9ucykge1xuICB2YXIgbGVuID0gYXJndW1lbnRzLmxlbmd0aDtcbiAgaWYgKGxlbiA9PT0gMSAmJiBBcnJheS5pc0FycmF5KGluaXRpYWxDb250YWluZXJzKSA9PT0gZmFsc2UpIHtcbiAgICBvcHRpb25zID0gaW5pdGlhbENvbnRhaW5lcnM7XG4gICAgaW5pdGlhbENvbnRhaW5lcnMgPSBbXTtcbiAgfVxuICB2YXIgX21pcnJvcjsgLy8gbWlycm9yIGltYWdlXG4gIHZhciBfc291cmNlOyAvLyBzb3VyY2UgY29udGFpbmVyXG4gIHZhciBfaXRlbTsgLy8gaXRlbSBiZWluZyBkcmFnZ2VkXG4gIHZhciBfb2Zmc2V0WDsgLy8gcmVmZXJlbmNlIHhcbiAgdmFyIF9vZmZzZXRZOyAvLyByZWZlcmVuY2UgeVxuICB2YXIgX21vdmVYOyAvLyByZWZlcmVuY2UgbW92ZSB4XG4gIHZhciBfbW92ZVk7IC8vIHJlZmVyZW5jZSBtb3ZlIHlcbiAgdmFyIF9pbml0aWFsU2libGluZzsgLy8gcmVmZXJlbmNlIHNpYmxpbmcgd2hlbiBncmFiYmVkXG4gIHZhciBfY3VycmVudFNpYmxpbmc7IC8vIHJlZmVyZW5jZSBzaWJsaW5nIG5vd1xuICB2YXIgX2NvcHk7IC8vIGl0ZW0gdXNlZCBmb3IgY29weWluZ1xuICB2YXIgX3JlbmRlclRpbWVyOyAvLyB0aW1lciBmb3Igc2V0VGltZW91dCByZW5kZXJNaXJyb3JJbWFnZVxuICB2YXIgX2xhc3REcm9wVGFyZ2V0ID0gbnVsbDsgLy8gbGFzdCBjb250YWluZXIgaXRlbSB3YXMgb3ZlclxuICB2YXIgX2dyYWJiZWQ7IC8vIGhvbGRzIG1vdXNlZG93biBjb250ZXh0IHVudGlsIGZpcnN0IG1vdXNlbW92ZVxuXG4gIHZhciBvID0gb3B0aW9ucyB8fCB7fTtcbiAgaWYgKG8ubW92ZXMgPT09IHZvaWQgMCkgeyBvLm1vdmVzID0gYWx3YXlzOyB9XG4gIGlmIChvLmFjY2VwdHMgPT09IHZvaWQgMCkgeyBvLmFjY2VwdHMgPSBhbHdheXM7IH1cbiAgaWYgKG8uaW52YWxpZCA9PT0gdm9pZCAwKSB7IG8uaW52YWxpZCA9IGludmFsaWRUYXJnZXQ7IH1cbiAgaWYgKG8uY29udGFpbmVycyA9PT0gdm9pZCAwKSB7IG8uY29udGFpbmVycyA9IGluaXRpYWxDb250YWluZXJzIHx8IFtdOyB9XG4gIGlmIChvLmlzQ29udGFpbmVyID09PSB2b2lkIDApIHsgby5pc0NvbnRhaW5lciA9IG5ldmVyOyB9XG4gIGlmIChvLmNvcHkgPT09IHZvaWQgMCkgeyBvLmNvcHkgPSBmYWxzZTsgfVxuICBpZiAoby5jb3B5U29ydFNvdXJjZSA9PT0gdm9pZCAwKSB7IG8uY29weVNvcnRTb3VyY2UgPSBmYWxzZTsgfVxuICBpZiAoby5yZXZlcnRPblNwaWxsID09PSB2b2lkIDApIHsgby5yZXZlcnRPblNwaWxsID0gZmFsc2U7IH1cbiAgaWYgKG8ucmVtb3ZlT25TcGlsbCA9PT0gdm9pZCAwKSB7IG8ucmVtb3ZlT25TcGlsbCA9IGZhbHNlOyB9XG4gIGlmIChvLmRpcmVjdGlvbiA9PT0gdm9pZCAwKSB7IG8uZGlyZWN0aW9uID0gJ3ZlcnRpY2FsJzsgfVxuICBpZiAoby5pZ25vcmVJbnB1dFRleHRTZWxlY3Rpb24gPT09IHZvaWQgMCkgeyBvLmlnbm9yZUlucHV0VGV4dFNlbGVjdGlvbiA9IHRydWU7IH1cbiAgaWYgKG8ubWlycm9yQ29udGFpbmVyID09PSB2b2lkIDApIHsgby5taXJyb3JDb250YWluZXIgPSBkb2MuYm9keTsgfVxuXG4gIHZhciBkcmFrZSA9IGVtaXR0ZXIoe1xuICAgIGNvbnRhaW5lcnM6IG8uY29udGFpbmVycyxcbiAgICBzdGFydDogbWFudWFsU3RhcnQsXG4gICAgZW5kOiBlbmQsXG4gICAgY2FuY2VsOiBjYW5jZWwsXG4gICAgcmVtb3ZlOiByZW1vdmUsXG4gICAgZGVzdHJveTogZGVzdHJveSxcbiAgICBjYW5Nb3ZlOiBjYW5Nb3ZlLFxuICAgIGRyYWdnaW5nOiBmYWxzZVxuICB9KTtcblxuICBpZiAoby5yZW1vdmVPblNwaWxsID09PSB0cnVlKSB7XG4gICAgZHJha2Uub24oJ292ZXInLCBzcGlsbE92ZXIpLm9uKCdvdXQnLCBzcGlsbE91dCk7XG4gIH1cblxuICBldmVudHMoKTtcblxuICByZXR1cm4gZHJha2U7XG5cbiAgZnVuY3Rpb24gaXNDb250YWluZXIgKGVsKSB7XG4gICAgcmV0dXJuIGRyYWtlLmNvbnRhaW5lcnMuaW5kZXhPZihlbCkgIT09IC0xIHx8IG8uaXNDb250YWluZXIoZWwpO1xuICB9XG5cbiAgZnVuY3Rpb24gZXZlbnRzIChyZW1vdmUpIHtcbiAgICB2YXIgb3AgPSByZW1vdmUgPyAncmVtb3ZlJyA6ICdhZGQnO1xuICAgIHRvdWNoeShkb2N1bWVudEVsZW1lbnQsIG9wLCAnbW91c2Vkb3duJywgZ3JhYik7XG4gICAgdG91Y2h5KGRvY3VtZW50RWxlbWVudCwgb3AsICdtb3VzZXVwJywgcmVsZWFzZSk7XG4gIH1cblxuICBmdW5jdGlvbiBldmVudHVhbE1vdmVtZW50cyAocmVtb3ZlKSB7XG4gICAgdmFyIG9wID0gcmVtb3ZlID8gJ3JlbW92ZScgOiAnYWRkJztcbiAgICB0b3VjaHkoZG9jdW1lbnRFbGVtZW50LCBvcCwgJ21vdXNlbW92ZScsIHN0YXJ0QmVjYXVzZU1vdXNlTW92ZWQpO1xuICB9XG5cbiAgZnVuY3Rpb24gbW92ZW1lbnRzIChyZW1vdmUpIHtcbiAgICB2YXIgb3AgPSByZW1vdmUgPyAncmVtb3ZlJyA6ICdhZGQnO1xuICAgIGNyb3NzdmVudFtvcF0oZG9jdW1lbnRFbGVtZW50LCAnc2VsZWN0c3RhcnQnLCBwcmV2ZW50R3JhYmJlZCk7IC8vIElFOFxuICAgIGNyb3NzdmVudFtvcF0oZG9jdW1lbnRFbGVtZW50LCAnY2xpY2snLCBwcmV2ZW50R3JhYmJlZCk7XG4gIH1cblxuICBmdW5jdGlvbiBkZXN0cm95ICgpIHtcbiAgICBldmVudHModHJ1ZSk7XG4gICAgcmVsZWFzZSh7fSk7XG4gIH1cblxuICBmdW5jdGlvbiBwcmV2ZW50R3JhYmJlZCAoZSkge1xuICAgIGlmIChfZ3JhYmJlZCkge1xuICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xuICAgIH1cbiAgfVxuXG4gIGZ1bmN0aW9uIGdyYWIgKGUpIHtcbiAgICBfbW92ZVggPSBlLmNsaWVudFg7XG4gICAgX21vdmVZID0gZS5jbGllbnRZO1xuXG4gICAgdmFyIGlnbm9yZSA9IHdoaWNoTW91c2VCdXR0b24oZSkgIT09IDEgfHwgZS5tZXRhS2V5IHx8IGUuY3RybEtleTtcbiAgICBpZiAoaWdub3JlKSB7XG4gICAgICByZXR1cm47IC8vIHdlIG9ubHkgY2FyZSBhYm91dCBob25lc3QtdG8tZ29kIGxlZnQgY2xpY2tzIGFuZCB0b3VjaCBldmVudHNcbiAgICB9XG4gICAgdmFyIGl0ZW0gPSBlLnRhcmdldDtcbiAgICB2YXIgY29udGV4dCA9IGNhblN0YXJ0KGl0ZW0pO1xuICAgIGlmICghY29udGV4dCkge1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBfZ3JhYmJlZCA9IGNvbnRleHQ7XG4gICAgZXZlbnR1YWxNb3ZlbWVudHMoKTtcbiAgICBpZiAoZS50eXBlID09PSAnbW91c2Vkb3duJykge1xuICAgICAgaWYgKGlzSW5wdXQoaXRlbSkpIHsgLy8gc2VlIGFsc286IGh0dHBzOi8vZ2l0aHViLmNvbS9iZXZhY3F1YS9kcmFndWxhL2lzc3Vlcy8yMDhcbiAgICAgICAgaXRlbS5mb2N1cygpOyAvLyBmaXhlcyBodHRwczovL2dpdGh1Yi5jb20vYmV2YWNxdWEvZHJhZ3VsYS9pc3N1ZXMvMTc2XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBlLnByZXZlbnREZWZhdWx0KCk7IC8vIGZpeGVzIGh0dHBzOi8vZ2l0aHViLmNvbS9iZXZhY3F1YS9kcmFndWxhL2lzc3Vlcy8xNTVcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICBmdW5jdGlvbiBzdGFydEJlY2F1c2VNb3VzZU1vdmVkIChlKSB7XG4gICAgaWYgKCFfZ3JhYmJlZCkge1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAod2hpY2hNb3VzZUJ1dHRvbihlKSA9PT0gMCkge1xuICAgICAgcmVsZWFzZSh7fSk7XG4gICAgICByZXR1cm47IC8vIHdoZW4gdGV4dCBpcyBzZWxlY3RlZCBvbiBhbiBpbnB1dCBhbmQgdGhlbiBkcmFnZ2VkLCBtb3VzZXVwIGRvZXNuJ3QgZmlyZS4gdGhpcyBpcyBvdXIgb25seSBob3BlXG4gICAgfVxuICAgIC8vIHRydXRoeSBjaGVjayBmaXhlcyAjMjM5LCBlcXVhbGl0eSBmaXhlcyAjMjA3XG4gICAgaWYgKGUuY2xpZW50WCAhPT0gdm9pZCAwICYmIGUuY2xpZW50WCA9PT0gX21vdmVYICYmIGUuY2xpZW50WSAhPT0gdm9pZCAwICYmIGUuY2xpZW50WSA9PT0gX21vdmVZKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChvLmlnbm9yZUlucHV0VGV4dFNlbGVjdGlvbikge1xuICAgICAgdmFyIGNsaWVudFggPSBnZXRDb29yZCgnY2xpZW50WCcsIGUpO1xuICAgICAgdmFyIGNsaWVudFkgPSBnZXRDb29yZCgnY2xpZW50WScsIGUpO1xuICAgICAgdmFyIGVsZW1lbnRCZWhpbmRDdXJzb3IgPSBkb2MuZWxlbWVudEZyb21Qb2ludChjbGllbnRYLCBjbGllbnRZKTtcbiAgICAgIGlmIChpc0lucHV0KGVsZW1lbnRCZWhpbmRDdXJzb3IpKSB7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICB9XG5cbiAgICB2YXIgZ3JhYmJlZCA9IF9ncmFiYmVkOyAvLyBjYWxsIHRvIGVuZCgpIHVuc2V0cyBfZ3JhYmJlZFxuICAgIGV2ZW50dWFsTW92ZW1lbnRzKHRydWUpO1xuICAgIG1vdmVtZW50cygpO1xuICAgIGVuZCgpO1xuICAgIHN0YXJ0KGdyYWJiZWQpO1xuXG4gICAgdmFyIG9mZnNldCA9IGdldE9mZnNldChfaXRlbSk7XG4gICAgX29mZnNldFggPSBnZXRDb29yZCgncGFnZVgnLCBlKSAtIG9mZnNldC5sZWZ0O1xuICAgIF9vZmZzZXRZID0gZ2V0Q29vcmQoJ3BhZ2VZJywgZSkgLSBvZmZzZXQudG9wO1xuXG4gICAgY2xhc3Nlcy5hZGQoX2NvcHkgfHwgX2l0ZW0sICdndS10cmFuc2l0Jyk7XG4gICAgcmVuZGVyTWlycm9ySW1hZ2UoKTtcbiAgICBkcmFnKGUpO1xuICB9XG5cbiAgZnVuY3Rpb24gY2FuU3RhcnQgKGl0ZW0pIHtcbiAgICBpZiAoZHJha2UuZHJhZ2dpbmcgJiYgX21pcnJvcikge1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAoaXNDb250YWluZXIoaXRlbSkpIHtcbiAgICAgIHJldHVybjsgLy8gZG9uJ3QgZHJhZyBjb250YWluZXIgaXRzZWxmXG4gICAgfVxuICAgIHZhciBoYW5kbGUgPSBpdGVtO1xuICAgIHdoaWxlIChnZXRQYXJlbnQoaXRlbSkgJiYgaXNDb250YWluZXIoZ2V0UGFyZW50KGl0ZW0pKSA9PT0gZmFsc2UpIHtcbiAgICAgIGlmIChvLmludmFsaWQoaXRlbSwgaGFuZGxlKSkge1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG4gICAgICBpdGVtID0gZ2V0UGFyZW50KGl0ZW0pOyAvLyBkcmFnIHRhcmdldCBzaG91bGQgYmUgYSB0b3AgZWxlbWVudFxuICAgICAgaWYgKCFpdGVtKSB7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICB9XG4gICAgdmFyIHNvdXJjZSA9IGdldFBhcmVudChpdGVtKTtcbiAgICBpZiAoIXNvdXJjZSkge1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAoby5pbnZhbGlkKGl0ZW0sIGhhbmRsZSkpIHtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICB2YXIgbW92YWJsZSA9IG8ubW92ZXMoaXRlbSwgc291cmNlLCBoYW5kbGUsIG5leHRFbChpdGVtKSk7XG4gICAgaWYgKCFtb3ZhYmxlKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgcmV0dXJuIHtcbiAgICAgIGl0ZW06IGl0ZW0sXG4gICAgICBzb3VyY2U6IHNvdXJjZVxuICAgIH07XG4gIH1cblxuICBmdW5jdGlvbiBjYW5Nb3ZlIChpdGVtKSB7XG4gICAgcmV0dXJuICEhY2FuU3RhcnQoaXRlbSk7XG4gIH1cblxuICBmdW5jdGlvbiBtYW51YWxTdGFydCAoaXRlbSkge1xuICAgIHZhciBjb250ZXh0ID0gY2FuU3RhcnQoaXRlbSk7XG4gICAgaWYgKGNvbnRleHQpIHtcbiAgICAgIHN0YXJ0KGNvbnRleHQpO1xuICAgIH1cbiAgfVxuXG4gIGZ1bmN0aW9uIHN0YXJ0IChjb250ZXh0KSB7XG4gICAgaWYgKGlzQ29weShjb250ZXh0Lml0ZW0sIGNvbnRleHQuc291cmNlKSkge1xuICAgICAgX2NvcHkgPSBjb250ZXh0Lml0ZW0uY2xvbmVOb2RlKHRydWUpO1xuICAgICAgZHJha2UuZW1pdCgnY2xvbmVkJywgX2NvcHksIGNvbnRleHQuaXRlbSwgJ2NvcHknKTtcbiAgICB9XG5cbiAgICBfc291cmNlID0gY29udGV4dC5zb3VyY2U7XG4gICAgX2l0ZW0gPSBjb250ZXh0Lml0ZW07XG4gICAgX2luaXRpYWxTaWJsaW5nID0gX2N1cnJlbnRTaWJsaW5nID0gbmV4dEVsKGNvbnRleHQuaXRlbSk7XG5cbiAgICBkcmFrZS5kcmFnZ2luZyA9IHRydWU7XG4gICAgZHJha2UuZW1pdCgnZHJhZycsIF9pdGVtLCBfc291cmNlKTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGludmFsaWRUYXJnZXQgKCkge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGVuZCAoKSB7XG4gICAgaWYgKCFkcmFrZS5kcmFnZ2luZykge1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB2YXIgaXRlbSA9IF9jb3B5IHx8IF9pdGVtO1xuICAgIGRyb3AoaXRlbSwgZ2V0UGFyZW50KGl0ZW0pKTtcbiAgfVxuXG4gIGZ1bmN0aW9uIHVuZ3JhYiAoKSB7XG4gICAgX2dyYWJiZWQgPSBmYWxzZTtcbiAgICBldmVudHVhbE1vdmVtZW50cyh0cnVlKTtcbiAgICBtb3ZlbWVudHModHJ1ZSk7XG4gIH1cblxuICBmdW5jdGlvbiByZWxlYXNlIChlKSB7XG4gICAgdW5ncmFiKCk7XG5cbiAgICBpZiAoIWRyYWtlLmRyYWdnaW5nKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIHZhciBpdGVtID0gX2NvcHkgfHwgX2l0ZW07XG4gICAgdmFyIGNsaWVudFggPSBnZXRDb29yZCgnY2xpZW50WCcsIGUpO1xuICAgIHZhciBjbGllbnRZID0gZ2V0Q29vcmQoJ2NsaWVudFknLCBlKTtcbiAgICB2YXIgZWxlbWVudEJlaGluZEN1cnNvciA9IGdldEVsZW1lbnRCZWhpbmRQb2ludChfbWlycm9yLCBjbGllbnRYLCBjbGllbnRZKTtcbiAgICB2YXIgZHJvcFRhcmdldCA9IGZpbmREcm9wVGFyZ2V0KGVsZW1lbnRCZWhpbmRDdXJzb3IsIGNsaWVudFgsIGNsaWVudFkpO1xuICAgIGlmIChkcm9wVGFyZ2V0ICYmICgoX2NvcHkgJiYgby5jb3B5U29ydFNvdXJjZSkgfHwgKCFfY29weSB8fCBkcm9wVGFyZ2V0ICE9PSBfc291cmNlKSkpIHtcbiAgICAgIGRyb3AoaXRlbSwgZHJvcFRhcmdldCk7XG4gICAgfSBlbHNlIGlmIChvLnJlbW92ZU9uU3BpbGwpIHtcbiAgICAgIHJlbW92ZSgpO1xuICAgIH0gZWxzZSB7XG4gICAgICBjYW5jZWwoKTtcbiAgICB9XG4gIH1cblxuICBmdW5jdGlvbiBkcm9wIChpdGVtLCB0YXJnZXQpIHtcbiAgICB2YXIgcGFyZW50ID0gZ2V0UGFyZW50KGl0ZW0pO1xuICAgIGlmIChfY29weSAmJiBvLmNvcHlTb3J0U291cmNlICYmIHRhcmdldCA9PT0gX3NvdXJjZSkge1xuICAgICAgcGFyZW50LnJlbW92ZUNoaWxkKF9pdGVtKTtcbiAgICB9XG4gICAgaWYgKGlzSW5pdGlhbFBsYWNlbWVudCh0YXJnZXQpKSB7XG4gICAgICBkcmFrZS5lbWl0KCdjYW5jZWwnLCBpdGVtLCBfc291cmNlLCBfc291cmNlKTtcbiAgICB9IGVsc2Uge1xuICAgICAgZHJha2UuZW1pdCgnZHJvcCcsIGl0ZW0sIHRhcmdldCwgX3NvdXJjZSwgX2N1cnJlbnRTaWJsaW5nKTtcbiAgICB9XG4gICAgY2xlYW51cCgpO1xuICB9XG5cbiAgZnVuY3Rpb24gcmVtb3ZlICgpIHtcbiAgICBpZiAoIWRyYWtlLmRyYWdnaW5nKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIHZhciBpdGVtID0gX2NvcHkgfHwgX2l0ZW07XG4gICAgdmFyIHBhcmVudCA9IGdldFBhcmVudChpdGVtKTtcbiAgICBpZiAocGFyZW50KSB7XG4gICAgICBwYXJlbnQucmVtb3ZlQ2hpbGQoaXRlbSk7XG4gICAgfVxuICAgIGRyYWtlLmVtaXQoX2NvcHkgPyAnY2FuY2VsJyA6ICdyZW1vdmUnLCBpdGVtLCBwYXJlbnQsIF9zb3VyY2UpO1xuICAgIGNsZWFudXAoKTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGNhbmNlbCAocmV2ZXJ0KSB7XG4gICAgaWYgKCFkcmFrZS5kcmFnZ2luZykge1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB2YXIgcmV2ZXJ0cyA9IGFyZ3VtZW50cy5sZW5ndGggPiAwID8gcmV2ZXJ0IDogby5yZXZlcnRPblNwaWxsO1xuICAgIHZhciBpdGVtID0gX2NvcHkgfHwgX2l0ZW07XG4gICAgdmFyIHBhcmVudCA9IGdldFBhcmVudChpdGVtKTtcbiAgICB2YXIgaW5pdGlhbCA9IGlzSW5pdGlhbFBsYWNlbWVudChwYXJlbnQpO1xuICAgIGlmIChpbml0aWFsID09PSBmYWxzZSAmJiByZXZlcnRzKSB7XG4gICAgICBpZiAoX2NvcHkpIHtcbiAgICAgICAgaWYgKHBhcmVudCkge1xuICAgICAgICAgIHBhcmVudC5yZW1vdmVDaGlsZChfY29weSk7XG4gICAgICAgIH1cbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIF9zb3VyY2UuaW5zZXJ0QmVmb3JlKGl0ZW0sIF9pbml0aWFsU2libGluZyk7XG4gICAgICB9XG4gICAgfVxuICAgIGlmIChpbml0aWFsIHx8IHJldmVydHMpIHtcbiAgICAgIGRyYWtlLmVtaXQoJ2NhbmNlbCcsIGl0ZW0sIF9zb3VyY2UsIF9zb3VyY2UpO1xuICAgIH0gZWxzZSB7XG4gICAgICBkcmFrZS5lbWl0KCdkcm9wJywgaXRlbSwgcGFyZW50LCBfc291cmNlLCBfY3VycmVudFNpYmxpbmcpO1xuICAgIH1cbiAgICBjbGVhbnVwKCk7XG4gIH1cblxuICBmdW5jdGlvbiBjbGVhbnVwICgpIHtcbiAgICB2YXIgaXRlbSA9IF9jb3B5IHx8IF9pdGVtO1xuICAgIHVuZ3JhYigpO1xuICAgIHJlbW92ZU1pcnJvckltYWdlKCk7XG4gICAgaWYgKGl0ZW0pIHtcbiAgICAgIGNsYXNzZXMucm0oaXRlbSwgJ2d1LXRyYW5zaXQnKTtcbiAgICB9XG4gICAgaWYgKF9yZW5kZXJUaW1lcikge1xuICAgICAgY2xlYXJUaW1lb3V0KF9yZW5kZXJUaW1lcik7XG4gICAgfVxuICAgIGRyYWtlLmRyYWdnaW5nID0gZmFsc2U7XG4gICAgaWYgKF9sYXN0RHJvcFRhcmdldCkge1xuICAgICAgZHJha2UuZW1pdCgnb3V0JywgaXRlbSwgX2xhc3REcm9wVGFyZ2V0LCBfc291cmNlKTtcbiAgICB9XG4gICAgZHJha2UuZW1pdCgnZHJhZ2VuZCcsIGl0ZW0pO1xuICAgIF9zb3VyY2UgPSBfaXRlbSA9IF9jb3B5ID0gX2luaXRpYWxTaWJsaW5nID0gX2N1cnJlbnRTaWJsaW5nID0gX3JlbmRlclRpbWVyID0gX2xhc3REcm9wVGFyZ2V0ID0gbnVsbDtcbiAgfVxuXG4gIGZ1bmN0aW9uIGlzSW5pdGlhbFBsYWNlbWVudCAodGFyZ2V0LCBzKSB7XG4gICAgdmFyIHNpYmxpbmc7XG4gICAgaWYgKHMgIT09IHZvaWQgMCkge1xuICAgICAgc2libGluZyA9IHM7XG4gICAgfSBlbHNlIGlmIChfbWlycm9yKSB7XG4gICAgICBzaWJsaW5nID0gX2N1cnJlbnRTaWJsaW5nO1xuICAgIH0gZWxzZSB7XG4gICAgICBzaWJsaW5nID0gbmV4dEVsKF9jb3B5IHx8IF9pdGVtKTtcbiAgICB9XG4gICAgcmV0dXJuIHRhcmdldCA9PT0gX3NvdXJjZSAmJiBzaWJsaW5nID09PSBfaW5pdGlhbFNpYmxpbmc7XG4gIH1cblxuICBmdW5jdGlvbiBmaW5kRHJvcFRhcmdldCAoZWxlbWVudEJlaGluZEN1cnNvciwgY2xpZW50WCwgY2xpZW50WSkge1xuICAgIHZhciB0YXJnZXQgPSBlbGVtZW50QmVoaW5kQ3Vyc29yO1xuICAgIHdoaWxlICh0YXJnZXQgJiYgIWFjY2VwdGVkKCkpIHtcbiAgICAgIHRhcmdldCA9IGdldFBhcmVudCh0YXJnZXQpO1xuICAgIH1cbiAgICByZXR1cm4gdGFyZ2V0O1xuXG4gICAgZnVuY3Rpb24gYWNjZXB0ZWQgKCkge1xuICAgICAgdmFyIGRyb3BwYWJsZSA9IGlzQ29udGFpbmVyKHRhcmdldCk7XG4gICAgICBpZiAoZHJvcHBhYmxlID09PSBmYWxzZSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICB9XG5cbiAgICAgIHZhciBpbW1lZGlhdGUgPSBnZXRJbW1lZGlhdGVDaGlsZCh0YXJnZXQsIGVsZW1lbnRCZWhpbmRDdXJzb3IpO1xuICAgICAgdmFyIHJlZmVyZW5jZSA9IGdldFJlZmVyZW5jZSh0YXJnZXQsIGltbWVkaWF0ZSwgY2xpZW50WCwgY2xpZW50WSk7XG4gICAgICB2YXIgaW5pdGlhbCA9IGlzSW5pdGlhbFBsYWNlbWVudCh0YXJnZXQsIHJlZmVyZW5jZSk7XG4gICAgICBpZiAoaW5pdGlhbCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTsgLy8gc2hvdWxkIGFsd2F5cyBiZSBhYmxlIHRvIGRyb3AgaXQgcmlnaHQgYmFjayB3aGVyZSBpdCB3YXNcbiAgICAgIH1cbiAgICAgIHJldHVybiBvLmFjY2VwdHMoX2l0ZW0sIHRhcmdldCwgX3NvdXJjZSwgcmVmZXJlbmNlKTtcbiAgICB9XG4gIH1cblxuICBmdW5jdGlvbiBkcmFnIChlKSB7XG4gICAgaWYgKCFfbWlycm9yKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGUucHJldmVudERlZmF1bHQoKTtcblxuICAgIHZhciBjbGllbnRYID0gZ2V0Q29vcmQoJ2NsaWVudFgnLCBlKTtcbiAgICB2YXIgY2xpZW50WSA9IGdldENvb3JkKCdjbGllbnRZJywgZSk7XG4gICAgdmFyIHggPSBjbGllbnRYIC0gX29mZnNldFg7XG4gICAgdmFyIHkgPSBjbGllbnRZIC0gX29mZnNldFk7XG5cbiAgICBfbWlycm9yLnN0eWxlLmxlZnQgPSB4ICsgJ3B4JztcbiAgICBfbWlycm9yLnN0eWxlLnRvcCA9IHkgKyAncHgnO1xuXG4gICAgdmFyIGl0ZW0gPSBfY29weSB8fCBfaXRlbTtcbiAgICB2YXIgZWxlbWVudEJlaGluZEN1cnNvciA9IGdldEVsZW1lbnRCZWhpbmRQb2ludChfbWlycm9yLCBjbGllbnRYLCBjbGllbnRZKTtcbiAgICB2YXIgZHJvcFRhcmdldCA9IGZpbmREcm9wVGFyZ2V0KGVsZW1lbnRCZWhpbmRDdXJzb3IsIGNsaWVudFgsIGNsaWVudFkpO1xuICAgIHZhciBjaGFuZ2VkID0gZHJvcFRhcmdldCAhPT0gbnVsbCAmJiBkcm9wVGFyZ2V0ICE9PSBfbGFzdERyb3BUYXJnZXQ7XG4gICAgaWYgKGNoYW5nZWQgfHwgZHJvcFRhcmdldCA9PT0gbnVsbCkge1xuICAgICAgb3V0KCk7XG4gICAgICBfbGFzdERyb3BUYXJnZXQgPSBkcm9wVGFyZ2V0O1xuICAgICAgb3ZlcigpO1xuICAgIH1cbiAgICB2YXIgcGFyZW50ID0gZ2V0UGFyZW50KGl0ZW0pO1xuICAgIGlmIChkcm9wVGFyZ2V0ID09PSBfc291cmNlICYmIF9jb3B5ICYmICFvLmNvcHlTb3J0U291cmNlKSB7XG4gICAgICBpZiAocGFyZW50KSB7XG4gICAgICAgIHBhcmVudC5yZW1vdmVDaGlsZChpdGVtKTtcbiAgICAgIH1cbiAgICAgIHJldHVybjtcbiAgICB9XG4gICAgdmFyIHJlZmVyZW5jZTtcbiAgICB2YXIgaW1tZWRpYXRlID0gZ2V0SW1tZWRpYXRlQ2hpbGQoZHJvcFRhcmdldCwgZWxlbWVudEJlaGluZEN1cnNvcik7XG4gICAgaWYgKGltbWVkaWF0ZSAhPT0gbnVsbCkge1xuICAgICAgcmVmZXJlbmNlID0gZ2V0UmVmZXJlbmNlKGRyb3BUYXJnZXQsIGltbWVkaWF0ZSwgY2xpZW50WCwgY2xpZW50WSk7XG4gICAgfSBlbHNlIGlmIChvLnJldmVydE9uU3BpbGwgPT09IHRydWUgJiYgIV9jb3B5KSB7XG4gICAgICByZWZlcmVuY2UgPSBfaW5pdGlhbFNpYmxpbmc7XG4gICAgICBkcm9wVGFyZ2V0ID0gX3NvdXJjZTtcbiAgICB9IGVsc2Uge1xuICAgICAgaWYgKF9jb3B5ICYmIHBhcmVudCkge1xuICAgICAgICBwYXJlbnQucmVtb3ZlQ2hpbGQoaXRlbSk7XG4gICAgICB9XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChcbiAgICAgIChyZWZlcmVuY2UgPT09IG51bGwgJiYgY2hhbmdlZCkgfHxcbiAgICAgIHJlZmVyZW5jZSAhPT0gaXRlbSAmJlxuICAgICAgcmVmZXJlbmNlICE9PSBuZXh0RWwoaXRlbSlcbiAgICApIHtcbiAgICAgIF9jdXJyZW50U2libGluZyA9IHJlZmVyZW5jZTtcbiAgICAgIGRyb3BUYXJnZXQuaW5zZXJ0QmVmb3JlKGl0ZW0sIHJlZmVyZW5jZSk7XG4gICAgICBkcmFrZS5lbWl0KCdzaGFkb3cnLCBpdGVtLCBkcm9wVGFyZ2V0LCBfc291cmNlKTtcbiAgICB9XG4gICAgZnVuY3Rpb24gbW92ZWQgKHR5cGUpIHsgZHJha2UuZW1pdCh0eXBlLCBpdGVtLCBfbGFzdERyb3BUYXJnZXQsIF9zb3VyY2UpOyB9XG4gICAgZnVuY3Rpb24gb3ZlciAoKSB7IGlmIChjaGFuZ2VkKSB7IG1vdmVkKCdvdmVyJyk7IH0gfVxuICAgIGZ1bmN0aW9uIG91dCAoKSB7IGlmIChfbGFzdERyb3BUYXJnZXQpIHsgbW92ZWQoJ291dCcpOyB9IH1cbiAgfVxuXG4gIGZ1bmN0aW9uIHNwaWxsT3ZlciAoZWwpIHtcbiAgICBjbGFzc2VzLnJtKGVsLCAnZ3UtaGlkZScpO1xuICB9XG5cbiAgZnVuY3Rpb24gc3BpbGxPdXQgKGVsKSB7XG4gICAgaWYgKGRyYWtlLmRyYWdnaW5nKSB7IGNsYXNzZXMuYWRkKGVsLCAnZ3UtaGlkZScpOyB9XG4gIH1cblxuICBmdW5jdGlvbiByZW5kZXJNaXJyb3JJbWFnZSAoKSB7XG4gICAgaWYgKF9taXJyb3IpIHtcbiAgICAgIHJldHVybjtcbiAgICB9XG4gICAgdmFyIHJlY3QgPSBfaXRlbS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcbiAgICBfbWlycm9yID0gX2l0ZW0uY2xvbmVOb2RlKHRydWUpO1xuICAgIF9taXJyb3Iuc3R5bGUud2lkdGggPSBnZXRSZWN0V2lkdGgocmVjdCkgKyAncHgnO1xuICAgIF9taXJyb3Iuc3R5bGUuaGVpZ2h0ID0gZ2V0UmVjdEhlaWdodChyZWN0KSArICdweCc7XG4gICAgY2xhc3Nlcy5ybShfbWlycm9yLCAnZ3UtdHJhbnNpdCcpO1xuICAgIGNsYXNzZXMuYWRkKF9taXJyb3IsICdndS1taXJyb3InKTtcbiAgICBvLm1pcnJvckNvbnRhaW5lci5hcHBlbmRDaGlsZChfbWlycm9yKTtcbiAgICB0b3VjaHkoZG9jdW1lbnRFbGVtZW50LCAnYWRkJywgJ21vdXNlbW92ZScsIGRyYWcpO1xuICAgIGNsYXNzZXMuYWRkKG8ubWlycm9yQ29udGFpbmVyLCAnZ3UtdW5zZWxlY3RhYmxlJyk7XG4gICAgZHJha2UuZW1pdCgnY2xvbmVkJywgX21pcnJvciwgX2l0ZW0sICdtaXJyb3InKTtcbiAgfVxuXG4gIGZ1bmN0aW9uIHJlbW92ZU1pcnJvckltYWdlICgpIHtcbiAgICBpZiAoX21pcnJvcikge1xuICAgICAgY2xhc3Nlcy5ybShvLm1pcnJvckNvbnRhaW5lciwgJ2d1LXVuc2VsZWN0YWJsZScpO1xuICAgICAgdG91Y2h5KGRvY3VtZW50RWxlbWVudCwgJ3JlbW92ZScsICdtb3VzZW1vdmUnLCBkcmFnKTtcbiAgICAgIGdldFBhcmVudChfbWlycm9yKS5yZW1vdmVDaGlsZChfbWlycm9yKTtcbiAgICAgIF9taXJyb3IgPSBudWxsO1xuICAgIH1cbiAgfVxuXG4gIGZ1bmN0aW9uIGdldEltbWVkaWF0ZUNoaWxkIChkcm9wVGFyZ2V0LCB0YXJnZXQpIHtcbiAgICB2YXIgaW1tZWRpYXRlID0gdGFyZ2V0O1xuICAgIHdoaWxlIChpbW1lZGlhdGUgIT09IGRyb3BUYXJnZXQgJiYgZ2V0UGFyZW50KGltbWVkaWF0ZSkgIT09IGRyb3BUYXJnZXQpIHtcbiAgICAgIGltbWVkaWF0ZSA9IGdldFBhcmVudChpbW1lZGlhdGUpO1xuICAgIH1cbiAgICBpZiAoaW1tZWRpYXRlID09PSBkb2N1bWVudEVsZW1lbnQpIHtcbiAgICAgIHJldHVybiBudWxsO1xuICAgIH1cbiAgICByZXR1cm4gaW1tZWRpYXRlO1xuICB9XG5cbiAgZnVuY3Rpb24gZ2V0UmVmZXJlbmNlIChkcm9wVGFyZ2V0LCB0YXJnZXQsIHgsIHkpIHtcbiAgICB2YXIgaG9yaXpvbnRhbCA9IG8uZGlyZWN0aW9uID09PSAnaG9yaXpvbnRhbCc7XG4gICAgdmFyIHJlZmVyZW5jZSA9IHRhcmdldCAhPT0gZHJvcFRhcmdldCA/IGluc2lkZSgpIDogb3V0c2lkZSgpO1xuICAgIHJldHVybiByZWZlcmVuY2U7XG5cbiAgICBmdW5jdGlvbiBvdXRzaWRlICgpIHsgLy8gc2xvd2VyLCBidXQgYWJsZSB0byBmaWd1cmUgb3V0IGFueSBwb3NpdGlvblxuICAgICAgdmFyIGxlbiA9IGRyb3BUYXJnZXQuY2hpbGRyZW4ubGVuZ3RoO1xuICAgICAgdmFyIGk7XG4gICAgICB2YXIgZWw7XG4gICAgICB2YXIgcmVjdDtcbiAgICAgIGZvciAoaSA9IDA7IGkgPCBsZW47IGkrKykge1xuICAgICAgICBlbCA9IGRyb3BUYXJnZXQuY2hpbGRyZW5baV07XG4gICAgICAgIHJlY3QgPSBlbC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcbiAgICAgICAgaWYgKGhvcml6b250YWwgJiYgKHJlY3QubGVmdCArIHJlY3Qud2lkdGggLyAyKSA+IHgpIHsgcmV0dXJuIGVsOyB9XG4gICAgICAgIGlmICghaG9yaXpvbnRhbCAmJiAocmVjdC50b3AgKyByZWN0LmhlaWdodCAvIDIpID4geSkgeyByZXR1cm4gZWw7IH1cbiAgICAgIH1cbiAgICAgIHJldHVybiBudWxsO1xuICAgIH1cblxuICAgIGZ1bmN0aW9uIGluc2lkZSAoKSB7IC8vIGZhc3RlciwgYnV0IG9ubHkgYXZhaWxhYmxlIGlmIGRyb3BwZWQgaW5zaWRlIGEgY2hpbGQgZWxlbWVudFxuICAgICAgdmFyIHJlY3QgPSB0YXJnZXQuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG4gICAgICBpZiAoaG9yaXpvbnRhbCkge1xuICAgICAgICByZXR1cm4gcmVzb2x2ZSh4ID4gcmVjdC5sZWZ0ICsgZ2V0UmVjdFdpZHRoKHJlY3QpIC8gMik7XG4gICAgICB9XG4gICAgICByZXR1cm4gcmVzb2x2ZSh5ID4gcmVjdC50b3AgKyBnZXRSZWN0SGVpZ2h0KHJlY3QpIC8gMik7XG4gICAgfVxuXG4gICAgZnVuY3Rpb24gcmVzb2x2ZSAoYWZ0ZXIpIHtcbiAgICAgIHJldHVybiBhZnRlciA/IG5leHRFbCh0YXJnZXQpIDogdGFyZ2V0O1xuICAgIH1cbiAgfVxuXG4gIGZ1bmN0aW9uIGlzQ29weSAoaXRlbSwgY29udGFpbmVyKSB7XG4gICAgcmV0dXJuIHR5cGVvZiBvLmNvcHkgPT09ICdib29sZWFuJyA/IG8uY29weSA6IG8uY29weShpdGVtLCBjb250YWluZXIpO1xuICB9XG59XG5cbmZ1bmN0aW9uIHRvdWNoeSAoZWwsIG9wLCB0eXBlLCBmbikge1xuICB2YXIgdG91Y2ggPSB7XG4gICAgbW91c2V1cDogJ3RvdWNoZW5kJyxcbiAgICBtb3VzZWRvd246ICd0b3VjaHN0YXJ0JyxcbiAgICBtb3VzZW1vdmU6ICd0b3VjaG1vdmUnXG4gIH07XG4gIHZhciBwb2ludGVycyA9IHtcbiAgICBtb3VzZXVwOiAncG9pbnRlcnVwJyxcbiAgICBtb3VzZWRvd246ICdwb2ludGVyZG93bicsXG4gICAgbW91c2Vtb3ZlOiAncG9pbnRlcm1vdmUnXG4gIH07XG4gIHZhciBtaWNyb3NvZnQgPSB7XG4gICAgbW91c2V1cDogJ01TUG9pbnRlclVwJyxcbiAgICBtb3VzZWRvd246ICdNU1BvaW50ZXJEb3duJyxcbiAgICBtb3VzZW1vdmU6ICdNU1BvaW50ZXJNb3ZlJ1xuICB9O1xuICBpZiAoZ2xvYmFsLm5hdmlnYXRvci5wb2ludGVyRW5hYmxlZCkge1xuICAgIGNyb3NzdmVudFtvcF0oZWwsIHBvaW50ZXJzW3R5cGVdLCBmbik7XG4gIH0gZWxzZSBpZiAoZ2xvYmFsLm5hdmlnYXRvci5tc1BvaW50ZXJFbmFibGVkKSB7XG4gICAgY3Jvc3N2ZW50W29wXShlbCwgbWljcm9zb2Z0W3R5cGVdLCBmbik7XG4gIH0gZWxzZSB7XG4gICAgY3Jvc3N2ZW50W29wXShlbCwgdG91Y2hbdHlwZV0sIGZuKTtcbiAgICBjcm9zc3ZlbnRbb3BdKGVsLCB0eXBlLCBmbik7XG4gIH1cbn1cblxuZnVuY3Rpb24gd2hpY2hNb3VzZUJ1dHRvbiAoZSkge1xuICBpZiAoZS50b3VjaGVzICE9PSB2b2lkIDApIHsgcmV0dXJuIGUudG91Y2hlcy5sZW5ndGg7IH1cbiAgaWYgKGUud2hpY2ggIT09IHZvaWQgMCAmJiBlLndoaWNoICE9PSAwKSB7IHJldHVybiBlLndoaWNoOyB9IC8vIHNlZSBodHRwczovL2dpdGh1Yi5jb20vYmV2YWNxdWEvZHJhZ3VsYS9pc3N1ZXMvMjYxXG4gIGlmIChlLmJ1dHRvbnMgIT09IHZvaWQgMCkgeyByZXR1cm4gZS5idXR0b25zOyB9XG4gIHZhciBidXR0b24gPSBlLmJ1dHRvbjtcbiAgaWYgKGJ1dHRvbiAhPT0gdm9pZCAwKSB7IC8vIHNlZSBodHRwczovL2dpdGh1Yi5jb20vanF1ZXJ5L2pxdWVyeS9ibG9iLzk5ZThmZjFiYWE3YWUzNDFlOTRiYjg5YzNlODQ1NzBjN2MzYWQ5ZWEvc3JjL2V2ZW50LmpzI0w1NzMtTDU3NVxuICAgIHJldHVybiBidXR0b24gJiAxID8gMSA6IGJ1dHRvbiAmIDIgPyAzIDogKGJ1dHRvbiAmIDQgPyAyIDogMCk7XG4gIH1cbn1cblxuZnVuY3Rpb24gZ2V0T2Zmc2V0IChlbCkge1xuICB2YXIgcmVjdCA9IGVsLmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuICByZXR1cm4ge1xuICAgIGxlZnQ6IHJlY3QubGVmdCArIGdldFNjcm9sbCgnc2Nyb2xsTGVmdCcsICdwYWdlWE9mZnNldCcpLFxuICAgIHRvcDogcmVjdC50b3AgKyBnZXRTY3JvbGwoJ3Njcm9sbFRvcCcsICdwYWdlWU9mZnNldCcpXG4gIH07XG59XG5cbmZ1bmN0aW9uIGdldFNjcm9sbCAoc2Nyb2xsUHJvcCwgb2Zmc2V0UHJvcCkge1xuICBpZiAodHlwZW9mIGdsb2JhbFtvZmZzZXRQcm9wXSAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICByZXR1cm4gZ2xvYmFsW29mZnNldFByb3BdO1xuICB9XG4gIGlmIChkb2N1bWVudEVsZW1lbnQuY2xpZW50SGVpZ2h0KSB7XG4gICAgcmV0dXJuIGRvY3VtZW50RWxlbWVudFtzY3JvbGxQcm9wXTtcbiAgfVxuICByZXR1cm4gZG9jLmJvZHlbc2Nyb2xsUHJvcF07XG59XG5cbmZ1bmN0aW9uIGdldEVsZW1lbnRCZWhpbmRQb2ludCAocG9pbnQsIHgsIHkpIHtcbiAgdmFyIHAgPSBwb2ludCB8fCB7fTtcbiAgdmFyIHN0YXRlID0gcC5jbGFzc05hbWU7XG4gIHZhciBlbDtcbiAgcC5jbGFzc05hbWUgKz0gJyBndS1oaWRlJztcbiAgZWwgPSBkb2MuZWxlbWVudEZyb21Qb2ludCh4LCB5KTtcbiAgcC5jbGFzc05hbWUgPSBzdGF0ZTtcbiAgcmV0dXJuIGVsO1xufVxuXG5mdW5jdGlvbiBuZXZlciAoKSB7IHJldHVybiBmYWxzZTsgfVxuZnVuY3Rpb24gYWx3YXlzICgpIHsgcmV0dXJuIHRydWU7IH1cbmZ1bmN0aW9uIGdldFJlY3RXaWR0aCAocmVjdCkgeyByZXR1cm4gcmVjdC53aWR0aCB8fCAocmVjdC5yaWdodCAtIHJlY3QubGVmdCk7IH1cbmZ1bmN0aW9uIGdldFJlY3RIZWlnaHQgKHJlY3QpIHsgcmV0dXJuIHJlY3QuaGVpZ2h0IHx8IChyZWN0LmJvdHRvbSAtIHJlY3QudG9wKTsgfVxuZnVuY3Rpb24gZ2V0UGFyZW50IChlbCkgeyByZXR1cm4gZWwucGFyZW50Tm9kZSA9PT0gZG9jID8gbnVsbCA6IGVsLnBhcmVudE5vZGU7IH1cbmZ1bmN0aW9uIGlzSW5wdXQgKGVsKSB7IHJldHVybiBlbC50YWdOYW1lID09PSAnSU5QVVQnIHx8IGVsLnRhZ05hbWUgPT09ICdURVhUQVJFQScgfHwgZWwudGFnTmFtZSA9PT0gJ1NFTEVDVCcgfHwgaXNFZGl0YWJsZShlbCk7IH1cbmZ1bmN0aW9uIGlzRWRpdGFibGUgKGVsKSB7XG4gIGlmICghZWwpIHsgcmV0dXJuIGZhbHNlOyB9IC8vIG5vIHBhcmVudHMgd2VyZSBlZGl0YWJsZVxuICBpZiAoZWwuY29udGVudEVkaXRhYmxlID09PSAnZmFsc2UnKSB7IHJldHVybiBmYWxzZTsgfSAvLyBzdG9wIHRoZSBsb29rdXBcbiAgaWYgKGVsLmNvbnRlbnRFZGl0YWJsZSA9PT0gJ3RydWUnKSB7IHJldHVybiB0cnVlOyB9IC8vIGZvdW5kIGEgY29udGVudEVkaXRhYmxlIGVsZW1lbnQgaW4gdGhlIGNoYWluXG4gIHJldHVybiBpc0VkaXRhYmxlKGdldFBhcmVudChlbCkpOyAvLyBjb250ZW50RWRpdGFibGUgaXMgc2V0IHRvICdpbmhlcml0J1xufVxuXG5mdW5jdGlvbiBuZXh0RWwgKGVsKSB7XG4gIHJldHVybiBlbC5uZXh0RWxlbWVudFNpYmxpbmcgfHwgbWFudWFsbHkoKTtcbiAgZnVuY3Rpb24gbWFudWFsbHkgKCkge1xuICAgIHZhciBzaWJsaW5nID0gZWw7XG4gICAgZG8ge1xuICAgICAgc2libGluZyA9IHNpYmxpbmcubmV4dFNpYmxpbmc7XG4gICAgfSB3aGlsZSAoc2libGluZyAmJiBzaWJsaW5nLm5vZGVUeXBlICE9PSAxKTtcbiAgICByZXR1cm4gc2libGluZztcbiAgfVxufVxuXG5mdW5jdGlvbiBnZXRFdmVudEhvc3QgKGUpIHtcbiAgLy8gb24gdG91Y2hlbmQgZXZlbnQsIHdlIGhhdmUgdG8gdXNlIGBlLmNoYW5nZWRUb3VjaGVzYFxuICAvLyBzZWUgaHR0cDovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy83MTkyNTYzL3RvdWNoZW5kLWV2ZW50LXByb3BlcnRpZXNcbiAgLy8gc2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9iZXZhY3F1YS9kcmFndWxhL2lzc3Vlcy8zNFxuICBpZiAoZS50YXJnZXRUb3VjaGVzICYmIGUudGFyZ2V0VG91Y2hlcy5sZW5ndGgpIHtcbiAgICByZXR1cm4gZS50YXJnZXRUb3VjaGVzWzBdO1xuICB9XG4gIGlmIChlLmNoYW5nZWRUb3VjaGVzICYmIGUuY2hhbmdlZFRvdWNoZXMubGVuZ3RoKSB7XG4gICAgcmV0dXJuIGUuY2hhbmdlZFRvdWNoZXNbMF07XG4gIH1cbiAgcmV0dXJuIGU7XG59XG5cbmZ1bmN0aW9uIGdldENvb3JkIChjb29yZCwgZSkge1xuICB2YXIgaG9zdCA9IGdldEV2ZW50SG9zdChlKTtcbiAgdmFyIG1pc3NNYXAgPSB7XG4gICAgcGFnZVg6ICdjbGllbnRYJywgLy8gSUU4XG4gICAgcGFnZVk6ICdjbGllbnRZJyAvLyBJRThcbiAgfTtcbiAgaWYgKGNvb3JkIGluIG1pc3NNYXAgJiYgIShjb29yZCBpbiBob3N0KSAmJiBtaXNzTWFwW2Nvb3JkXSBpbiBob3N0KSB7XG4gICAgY29vcmQgPSBtaXNzTWFwW2Nvb3JkXTtcbiAgfVxuICByZXR1cm4gaG9zdFtjb29yZF07XG59XG5cbm1vZHVsZS5leHBvcnRzID0gZHJhZ3VsYTtcbiIsIm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gYXRvYSAoYSwgbikgeyByZXR1cm4gQXJyYXkucHJvdG90eXBlLnNsaWNlLmNhbGwoYSwgbik7IH1cbiIsIid1c2Ugc3RyaWN0JztcblxudmFyIHRpY2t5ID0gcmVxdWlyZSgndGlja3knKTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBkZWJvdW5jZSAoZm4sIGFyZ3MsIGN0eCkge1xuICBpZiAoIWZuKSB7IHJldHVybjsgfVxuICB0aWNreShmdW5jdGlvbiBydW4gKCkge1xuICAgIGZuLmFwcGx5KGN0eCB8fCBudWxsLCBhcmdzIHx8IFtdKTtcbiAgfSk7XG59O1xuIiwiJ3VzZSBzdHJpY3QnO1xuXG52YXIgYXRvYSA9IHJlcXVpcmUoJ2F0b2EnKTtcbnZhciBkZWJvdW5jZSA9IHJlcXVpcmUoJy4vZGVib3VuY2UnKTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBlbWl0dGVyICh0aGluZywgb3B0aW9ucykge1xuICB2YXIgb3B0cyA9IG9wdGlvbnMgfHwge307XG4gIHZhciBldnQgPSB7fTtcbiAgaWYgKHRoaW5nID09PSB1bmRlZmluZWQpIHsgdGhpbmcgPSB7fTsgfVxuICB0aGluZy5vbiA9IGZ1bmN0aW9uICh0eXBlLCBmbikge1xuICAgIGlmICghZXZ0W3R5cGVdKSB7XG4gICAgICBldnRbdHlwZV0gPSBbZm5dO1xuICAgIH0gZWxzZSB7XG4gICAgICBldnRbdHlwZV0ucHVzaChmbik7XG4gICAgfVxuICAgIHJldHVybiB0aGluZztcbiAgfTtcbiAgdGhpbmcub25jZSA9IGZ1bmN0aW9uICh0eXBlLCBmbikge1xuICAgIGZuLl9vbmNlID0gdHJ1ZTsgLy8gdGhpbmcub2ZmKGZuKSBzdGlsbCB3b3JrcyFcbiAgICB0aGluZy5vbih0eXBlLCBmbik7XG4gICAgcmV0dXJuIHRoaW5nO1xuICB9O1xuICB0aGluZy5vZmYgPSBmdW5jdGlvbiAodHlwZSwgZm4pIHtcbiAgICB2YXIgYyA9IGFyZ3VtZW50cy5sZW5ndGg7XG4gICAgaWYgKGMgPT09IDEpIHtcbiAgICAgIGRlbGV0ZSBldnRbdHlwZV07XG4gICAgfSBlbHNlIGlmIChjID09PSAwKSB7XG4gICAgICBldnQgPSB7fTtcbiAgICB9IGVsc2Uge1xuICAgICAgdmFyIGV0ID0gZXZ0W3R5cGVdO1xuICAgICAgaWYgKCFldCkgeyByZXR1cm4gdGhpbmc7IH1cbiAgICAgIGV0LnNwbGljZShldC5pbmRleE9mKGZuKSwgMSk7XG4gICAgfVxuICAgIHJldHVybiB0aGluZztcbiAgfTtcbiAgdGhpbmcuZW1pdCA9IGZ1bmN0aW9uICgpIHtcbiAgICB2YXIgYXJncyA9IGF0b2EoYXJndW1lbnRzKTtcbiAgICByZXR1cm4gdGhpbmcuZW1pdHRlclNuYXBzaG90KGFyZ3Muc2hpZnQoKSkuYXBwbHkodGhpcywgYXJncyk7XG4gIH07XG4gIHRoaW5nLmVtaXR0ZXJTbmFwc2hvdCA9IGZ1bmN0aW9uICh0eXBlKSB7XG4gICAgdmFyIGV0ID0gKGV2dFt0eXBlXSB8fCBbXSkuc2xpY2UoMCk7XG4gICAgcmV0dXJuIGZ1bmN0aW9uICgpIHtcbiAgICAgIHZhciBhcmdzID0gYXRvYShhcmd1bWVudHMpO1xuICAgICAgdmFyIGN0eCA9IHRoaXMgfHwgdGhpbmc7XG4gICAgICBpZiAodHlwZSA9PT0gJ2Vycm9yJyAmJiBvcHRzLnRocm93cyAhPT0gZmFsc2UgJiYgIWV0Lmxlbmd0aCkgeyB0aHJvdyBhcmdzLmxlbmd0aCA9PT0gMSA/IGFyZ3NbMF0gOiBhcmdzOyB9XG4gICAgICBldC5mb3JFYWNoKGZ1bmN0aW9uIGVtaXR0ZXIgKGxpc3Rlbikge1xuICAgICAgICBpZiAob3B0cy5hc3luYykgeyBkZWJvdW5jZShsaXN0ZW4sIGFyZ3MsIGN0eCk7IH0gZWxzZSB7IGxpc3Rlbi5hcHBseShjdHgsIGFyZ3MpOyB9XG4gICAgICAgIGlmIChsaXN0ZW4uX29uY2UpIHsgdGhpbmcub2ZmKHR5cGUsIGxpc3Rlbik7IH1cbiAgICAgIH0pO1xuICAgICAgcmV0dXJuIHRoaW5nO1xuICAgIH07XG4gIH07XG4gIHJldHVybiB0aGluZztcbn07XG4iLCIndXNlIHN0cmljdCc7XG5cbnZhciBjdXN0b21FdmVudCA9IHJlcXVpcmUoJ2N1c3RvbS1ldmVudCcpO1xudmFyIGV2ZW50bWFwID0gcmVxdWlyZSgnLi9ldmVudG1hcCcpO1xudmFyIGRvYyA9IGdsb2JhbC5kb2N1bWVudDtcbnZhciBhZGRFdmVudCA9IGFkZEV2ZW50RWFzeTtcbnZhciByZW1vdmVFdmVudCA9IHJlbW92ZUV2ZW50RWFzeTtcbnZhciBoYXJkQ2FjaGUgPSBbXTtcblxuaWYgKCFnbG9iYWwuYWRkRXZlbnRMaXN0ZW5lcikge1xuICBhZGRFdmVudCA9IGFkZEV2ZW50SGFyZDtcbiAgcmVtb3ZlRXZlbnQgPSByZW1vdmVFdmVudEhhcmQ7XG59XG5cbm1vZHVsZS5leHBvcnRzID0ge1xuICBhZGQ6IGFkZEV2ZW50LFxuICByZW1vdmU6IHJlbW92ZUV2ZW50LFxuICBmYWJyaWNhdGU6IGZhYnJpY2F0ZUV2ZW50XG59O1xuXG5mdW5jdGlvbiBhZGRFdmVudEVhc3kgKGVsLCB0eXBlLCBmbiwgY2FwdHVyaW5nKSB7XG4gIHJldHVybiBlbC5hZGRFdmVudExpc3RlbmVyKHR5cGUsIGZuLCBjYXB0dXJpbmcpO1xufVxuXG5mdW5jdGlvbiBhZGRFdmVudEhhcmQgKGVsLCB0eXBlLCBmbikge1xuICByZXR1cm4gZWwuYXR0YWNoRXZlbnQoJ29uJyArIHR5cGUsIHdyYXAoZWwsIHR5cGUsIGZuKSk7XG59XG5cbmZ1bmN0aW9uIHJlbW92ZUV2ZW50RWFzeSAoZWwsIHR5cGUsIGZuLCBjYXB0dXJpbmcpIHtcbiAgcmV0dXJuIGVsLnJlbW92ZUV2ZW50TGlzdGVuZXIodHlwZSwgZm4sIGNhcHR1cmluZyk7XG59XG5cbmZ1bmN0aW9uIHJlbW92ZUV2ZW50SGFyZCAoZWwsIHR5cGUsIGZuKSB7XG4gIHZhciBsaXN0ZW5lciA9IHVud3JhcChlbCwgdHlwZSwgZm4pO1xuICBpZiAobGlzdGVuZXIpIHtcbiAgICByZXR1cm4gZWwuZGV0YWNoRXZlbnQoJ29uJyArIHR5cGUsIGxpc3RlbmVyKTtcbiAgfVxufVxuXG5mdW5jdGlvbiBmYWJyaWNhdGVFdmVudCAoZWwsIHR5cGUsIG1vZGVsKSB7XG4gIHZhciBlID0gZXZlbnRtYXAuaW5kZXhPZih0eXBlKSA9PT0gLTEgPyBtYWtlQ3VzdG9tRXZlbnQoKSA6IG1ha2VDbGFzc2ljRXZlbnQoKTtcbiAgaWYgKGVsLmRpc3BhdGNoRXZlbnQpIHtcbiAgICBlbC5kaXNwYXRjaEV2ZW50KGUpO1xuICB9IGVsc2Uge1xuICAgIGVsLmZpcmVFdmVudCgnb24nICsgdHlwZSwgZSk7XG4gIH1cbiAgZnVuY3Rpb24gbWFrZUNsYXNzaWNFdmVudCAoKSB7XG4gICAgdmFyIGU7XG4gICAgaWYgKGRvYy5jcmVhdGVFdmVudCkge1xuICAgICAgZSA9IGRvYy5jcmVhdGVFdmVudCgnRXZlbnQnKTtcbiAgICAgIGUuaW5pdEV2ZW50KHR5cGUsIHRydWUsIHRydWUpO1xuICAgIH0gZWxzZSBpZiAoZG9jLmNyZWF0ZUV2ZW50T2JqZWN0KSB7XG4gICAgICBlID0gZG9jLmNyZWF0ZUV2ZW50T2JqZWN0KCk7XG4gICAgfVxuICAgIHJldHVybiBlO1xuICB9XG4gIGZ1bmN0aW9uIG1ha2VDdXN0b21FdmVudCAoKSB7XG4gICAgcmV0dXJuIG5ldyBjdXN0b21FdmVudCh0eXBlLCB7IGRldGFpbDogbW9kZWwgfSk7XG4gIH1cbn1cblxuZnVuY3Rpb24gd3JhcHBlckZhY3RvcnkgKGVsLCB0eXBlLCBmbikge1xuICByZXR1cm4gZnVuY3Rpb24gd3JhcHBlciAob3JpZ2luYWxFdmVudCkge1xuICAgIHZhciBlID0gb3JpZ2luYWxFdmVudCB8fCBnbG9iYWwuZXZlbnQ7XG4gICAgZS50YXJnZXQgPSBlLnRhcmdldCB8fCBlLnNyY0VsZW1lbnQ7XG4gICAgZS5wcmV2ZW50RGVmYXVsdCA9IGUucHJldmVudERlZmF1bHQgfHwgZnVuY3Rpb24gcHJldmVudERlZmF1bHQgKCkgeyBlLnJldHVyblZhbHVlID0gZmFsc2U7IH07XG4gICAgZS5zdG9wUHJvcGFnYXRpb24gPSBlLnN0b3BQcm9wYWdhdGlvbiB8fCBmdW5jdGlvbiBzdG9wUHJvcGFnYXRpb24gKCkgeyBlLmNhbmNlbEJ1YmJsZSA9IHRydWU7IH07XG4gICAgZS53aGljaCA9IGUud2hpY2ggfHwgZS5rZXlDb2RlO1xuICAgIGZuLmNhbGwoZWwsIGUpO1xuICB9O1xufVxuXG5mdW5jdGlvbiB3cmFwIChlbCwgdHlwZSwgZm4pIHtcbiAgdmFyIHdyYXBwZXIgPSB1bndyYXAoZWwsIHR5cGUsIGZuKSB8fCB3cmFwcGVyRmFjdG9yeShlbCwgdHlwZSwgZm4pO1xuICBoYXJkQ2FjaGUucHVzaCh7XG4gICAgd3JhcHBlcjogd3JhcHBlcixcbiAgICBlbGVtZW50OiBlbCxcbiAgICB0eXBlOiB0eXBlLFxuICAgIGZuOiBmblxuICB9KTtcbiAgcmV0dXJuIHdyYXBwZXI7XG59XG5cbmZ1bmN0aW9uIHVud3JhcCAoZWwsIHR5cGUsIGZuKSB7XG4gIHZhciBpID0gZmluZChlbCwgdHlwZSwgZm4pO1xuICBpZiAoaSkge1xuICAgIHZhciB3cmFwcGVyID0gaGFyZENhY2hlW2ldLndyYXBwZXI7XG4gICAgaGFyZENhY2hlLnNwbGljZShpLCAxKTsgLy8gZnJlZSB1cCBhIHRhZCBvZiBtZW1vcnlcbiAgICByZXR1cm4gd3JhcHBlcjtcbiAgfVxufVxuXG5mdW5jdGlvbiBmaW5kIChlbCwgdHlwZSwgZm4pIHtcbiAgdmFyIGksIGl0ZW07XG4gIGZvciAoaSA9IDA7IGkgPCBoYXJkQ2FjaGUubGVuZ3RoOyBpKyspIHtcbiAgICBpdGVtID0gaGFyZENhY2hlW2ldO1xuICAgIGlmIChpdGVtLmVsZW1lbnQgPT09IGVsICYmIGl0ZW0udHlwZSA9PT0gdHlwZSAmJiBpdGVtLmZuID09PSBmbikge1xuICAgICAgcmV0dXJuIGk7XG4gICAgfVxuICB9XG59XG4iLCIndXNlIHN0cmljdCc7XG5cbnZhciBldmVudG1hcCA9IFtdO1xudmFyIGV2ZW50bmFtZSA9ICcnO1xudmFyIHJvbiA9IC9eb24vO1xuXG5mb3IgKGV2ZW50bmFtZSBpbiBnbG9iYWwpIHtcbiAgaWYgKHJvbi50ZXN0KGV2ZW50bmFtZSkpIHtcbiAgICBldmVudG1hcC5wdXNoKGV2ZW50bmFtZS5zbGljZSgyKSk7XG4gIH1cbn1cblxubW9kdWxlLmV4cG9ydHMgPSBldmVudG1hcDtcbiIsIlxudmFyIE5hdGl2ZUN1c3RvbUV2ZW50ID0gZ2xvYmFsLkN1c3RvbUV2ZW50O1xuXG5mdW5jdGlvbiB1c2VOYXRpdmUgKCkge1xuICB0cnkge1xuICAgIHZhciBwID0gbmV3IE5hdGl2ZUN1c3RvbUV2ZW50KCdjYXQnLCB7IGRldGFpbDogeyBmb286ICdiYXInIH0gfSk7XG4gICAgcmV0dXJuICAnY2F0JyA9PT0gcC50eXBlICYmICdiYXInID09PSBwLmRldGFpbC5mb287XG4gIH0gY2F0Y2ggKGUpIHtcbiAgfVxuICByZXR1cm4gZmFsc2U7XG59XG5cbi8qKlxuICogQ3Jvc3MtYnJvd3NlciBgQ3VzdG9tRXZlbnRgIGNvbnN0cnVjdG9yLlxuICpcbiAqIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9DdXN0b21FdmVudC5DdXN0b21FdmVudFxuICpcbiAqIEBwdWJsaWNcbiAqL1xuXG5tb2R1bGUuZXhwb3J0cyA9IHVzZU5hdGl2ZSgpID8gTmF0aXZlQ3VzdG9tRXZlbnQgOlxuXG4vLyBJRSA+PSA5XG4nZnVuY3Rpb24nID09PSB0eXBlb2YgZG9jdW1lbnQuY3JlYXRlRXZlbnQgPyBmdW5jdGlvbiBDdXN0b21FdmVudCAodHlwZSwgcGFyYW1zKSB7XG4gIHZhciBlID0gZG9jdW1lbnQuY3JlYXRlRXZlbnQoJ0N1c3RvbUV2ZW50Jyk7XG4gIGlmIChwYXJhbXMpIHtcbiAgICBlLmluaXRDdXN0b21FdmVudCh0eXBlLCBwYXJhbXMuYnViYmxlcywgcGFyYW1zLmNhbmNlbGFibGUsIHBhcmFtcy5kZXRhaWwpO1xuICB9IGVsc2Uge1xuICAgIGUuaW5pdEN1c3RvbUV2ZW50KHR5cGUsIGZhbHNlLCBmYWxzZSwgdm9pZCAwKTtcbiAgfVxuICByZXR1cm4gZTtcbn0gOlxuXG4vLyBJRSA8PSA4XG5mdW5jdGlvbiBDdXN0b21FdmVudCAodHlwZSwgcGFyYW1zKSB7XG4gIHZhciBlID0gZG9jdW1lbnQuY3JlYXRlRXZlbnRPYmplY3QoKTtcbiAgZS50eXBlID0gdHlwZTtcbiAgaWYgKHBhcmFtcykge1xuICAgIGUuYnViYmxlcyA9IEJvb2xlYW4ocGFyYW1zLmJ1YmJsZXMpO1xuICAgIGUuY2FuY2VsYWJsZSA9IEJvb2xlYW4ocGFyYW1zLmNhbmNlbGFibGUpO1xuICAgIGUuZGV0YWlsID0gcGFyYW1zLmRldGFpbDtcbiAgfSBlbHNlIHtcbiAgICBlLmJ1YmJsZXMgPSBmYWxzZTtcbiAgICBlLmNhbmNlbGFibGUgPSBmYWxzZTtcbiAgICBlLmRldGFpbCA9IHZvaWQgMDtcbiAgfVxuICByZXR1cm4gZTtcbn1cbiIsInZhciBzaSA9IHR5cGVvZiBzZXRJbW1lZGlhdGUgPT09ICdmdW5jdGlvbicsIHRpY2s7XG5pZiAoc2kpIHtcbiAgdGljayA9IGZ1bmN0aW9uIChmbikgeyBzZXRJbW1lZGlhdGUoZm4pOyB9O1xufSBlbHNlIHtcbiAgdGljayA9IGZ1bmN0aW9uIChmbikgeyBzZXRUaW1lb3V0KGZuLCAwKTsgfTtcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB0aWNrOyJdfQ== diff --git a/pagure/static/vendor/dragula/dragula-3.7.2.min.js b/pagure/static/vendor/dragula/dragula-3.7.2.min.js new file mode 100644 index 0000000..41e2c18 --- /dev/null +++ b/pagure/static/vendor/dragula/dragula-3.7.2.min.js @@ -0,0 +1 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n;n="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,n.dragula=e()}}(function(){return function e(n,t,r){function o(u,c){if(!t[u]){if(!n[u]){var a="function"==typeof require&&require;if(!c&&a)return a(u,!0);if(i)return i(u,!0);var f=new Error("Cannot find module '"+u+"'");throw f.code="MODULE_NOT_FOUND",f}var l=t[u]={exports:{}};n[u][0].call(l.exports,function(e){var t=n[u][1][e];return o(t?t:e)},l,l.exports,e,n,t,r)}return t[u].exports}for(var i="function"==typeof require&&require,u=0;u0?e:fe.revertOnSpill,t=ie||W,r=v(t),o=k(r);o===!1&&n&&(ie?r&&r.removeChild(ie):Q.insertBefore(t,re)),o||n?le.emit("cancel",t,Q,Q):le.emit("drop",t,r,Q,oe),j()}}function j(){var e=ie||W;I(),z(),e&&E.rm(e,"gu-transit"),ue&&clearTimeout(ue),le.dragging=!1,ae&&le.emit("out",e,ae,Q),le.emit("dragend",e),Q=W=ie=re=oe=ue=ae=null}function k(e,n){var t;return t=void 0!==n?n:J?oe:g(ie||W),e===Q&&t===re}function q(e,n,r){function o(){var o=t(i);if(o===!1)return!1;var u=H(i,e),c=V(i,u,n,r),a=k(i,c);return a?!0:fe.accepts(W,i,Q,c)}for(var i=e;i&&!o();)i=v(i);return i}function U(e){function n(e){le.emit(e,f,ae,Q)}function t(){s&&n("over")}function r(){ae&&n("out")}if(J){e.preventDefault();var o=y("clientX",e),i=y("clientY",e),u=o-Z,c=i-ee;J.style.left=u+"px",J.style.top=c+"px";var f=ie||W,l=a(J,o,i),d=q(l,o,i),s=null!==d&&d!==ae;(s||null===d)&&(r(),ae=d,t());var p=v(f);if(d===Q&&ie&&!fe.copySortSource)return void(p&&p.removeChild(f));var m,h=H(d,l);if(null!==h)m=V(d,h,o,i);else{if(fe.revertOnSpill!==!0||ie)return void(ie&&p&&p.removeChild(f));m=re,d=Q}(null===m&&s||m!==f&&m!==g(f))&&(oe=m,d.insertBefore(f,m),le.emit("shadow",f,d,Q))}}function _(e){E.rm(e,"gu-hide")}function F(e){le.dragging&&E.add(e,"gu-hide")}function K(){if(!J){var e=W.getBoundingClientRect();J=W.cloneNode(!0),J.style.width=d(e)+"px",J.style.height=s(e)+"px",E.rm(J,"gu-transit"),E.add(J,"gu-mirror"),fe.mirrorContainer.appendChild(J),o(S,"add","mousemove",U),E.add(fe.mirrorContainer,"gu-unselectable"),le.emit("cloned",J,W,"mirror")}}function z(){J&&(E.rm(fe.mirrorContainer,"gu-unselectable"),o(S,"remove","mousemove",U),v(J).removeChild(J),J=null)}function H(e,n){for(var t=n;t!==e&&v(t)!==e;)t=v(t);return t===S?null:t}function V(e,n,t,r){function o(){var n,o,i,u=e.children.length;for(n=0;u>n;n++){if(o=e.children[n],i=o.getBoundingClientRect(),c&&i.left+i.width/2>t)return o;if(!c&&i.top+i.height/2>r)return o}return null}function i(){var e=n.getBoundingClientRect();return u(c?t>e.left+d(e)/2:r>e.top+s(e)/2)}function u(e){return e?g(n):n}var c="horizontal"===fe.direction,a=n!==e?i():o();return a}function $(e,n){return"boolean"==typeof fe.copy?fe.copy:fe.copy(e,n)}var G=arguments.length;1===G&&Array.isArray(e)===!1&&(n=e,e=[]);var J,Q,W,Z,ee,ne,te,re,oe,ie,ue,ce,ae=null,fe=n||{};void 0===fe.moves&&(fe.moves=l),void 0===fe.accepts&&(fe.accepts=l),void 0===fe.invalid&&(fe.invalid=P),void 0===fe.containers&&(fe.containers=e||[]),void 0===fe.isContainer&&(fe.isContainer=f),void 0===fe.copy&&(fe.copy=!1),void 0===fe.copySortSource&&(fe.copySortSource=!1),void 0===fe.revertOnSpill&&(fe.revertOnSpill=!1),void 0===fe.removeOnSpill&&(fe.removeOnSpill=!1),void 0===fe.direction&&(fe.direction="vertical"),void 0===fe.ignoreInputTextSelection&&(fe.ignoreInputTextSelection=!0),void 0===fe.mirrorContainer&&(fe.mirrorContainer=x.body);var le=b({containers:fe.containers,start:Y,end:D,cancel:A,remove:R,destroy:h,canMove:X,dragging:!1});return fe.removeOnSpill===!0&&le.on("over",_).on("out",F),r(),le}function o(e,n,r,o){var i={mouseup:"touchend",mousedown:"touchstart",mousemove:"touchmove"},u={mouseup:"pointerup",mousedown:"pointerdown",mousemove:"pointermove"},c={mouseup:"MSPointerUp",mousedown:"MSPointerDown",mousemove:"MSPointerMove"};t.navigator.pointerEnabled?w[n](e,u[r],o):t.navigator.msPointerEnabled?w[n](e,c[r],o):(w[n](e,i[r],o),w[n](e,r,o))}function i(e){if(void 0!==e.touches)return e.touches.length;if(void 0!==e.which&&0!==e.which)return e.which;if(void 0!==e.buttons)return e.buttons;var n=e.button;return void 0!==n?1&n?1:2&n?3:4&n?2:0:void 0}function u(e){var n=e.getBoundingClientRect();return{left:n.left+c("scrollLeft","pageXOffset"),top:n.top+c("scrollTop","pageYOffset")}}function c(e,n){return"undefined"!=typeof t[n]?t[n]:S.clientHeight?S[e]:x.body[e]}function a(e,n,t){var r,o=e||{},i=o.className;return o.className+=" gu-hide",r=x.elementFromPoint(n,t),o.className=i,r}function f(){return!1}function l(){return!0}function d(e){return e.width||e.right-e.left}function s(e){return e.height||e.bottom-e.top}function v(e){return e.parentNode===x?null:e.parentNode}function p(e){return"INPUT"===e.tagName||"TEXTAREA"===e.tagName||"SELECT"===e.tagName||m(e)}function m(e){return e?"false"===e.contentEditable?!1:"true"===e.contentEditable?!0:m(v(e)):!1}function g(e){function n(){var n=e;do n=n.nextSibling;while(n&&1!==n.nodeType);return n}return e.nextElementSibling||n()}function h(e){return e.targetTouches&&e.targetTouches.length?e.targetTouches[0]:e.changedTouches&&e.changedTouches.length?e.changedTouches[0]:e}function y(e,n){var t=h(n),r={pageX:"clientX",pageY:"clientY"};return e in r&&!(e in t)&&r[e]in t&&(e=r[e]),t[e]}var b=e("contra/emitter"),w=e("crossvent"),E=e("./classes"),x=document,S=x.documentElement;n.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./classes":1,"contra/emitter":5,crossvent:6}],3:[function(e,n,t){n.exports=function(e,n){return Array.prototype.slice.call(e,n)}},{}],4:[function(e,n,t){"use strict";var r=e("ticky");n.exports=function(e,n,t){e&&r(function(){e.apply(t||null,n||[])})}},{ticky:9}],5:[function(e,n,t){"use strict";var r=e("atoa"),o=e("./debounce");n.exports=function(e,n){var t=n||{},i={};return void 0===e&&(e={}),e.on=function(n,t){return i[n]?i[n].push(t):i[n]=[t],e},e.once=function(n,t){return t._once=!0,e.on(n,t),e},e.off=function(n,t){var r=arguments.length;if(1===r)delete i[n];else if(0===r)i={};else{var o=i[n];if(!o)return e;o.splice(o.indexOf(t),1)}return e},e.emit=function(){var n=r(arguments);return e.emitterSnapshot(n.shift()).apply(this,n)},e.emitterSnapshot=function(n){var u=(i[n]||[]).slice(0);return function(){var i=r(arguments),c=this||e;if("error"===n&&t["throws"]!==!1&&!u.length)throw 1===i.length?i[0]:i;return u.forEach(function(r){t.async?o(r,i,c):r.apply(c,i),r._once&&e.off(n,r)}),e}},e}},{"./debounce":4,atoa:3}],6:[function(e,n,t){(function(t){"use strict";function r(e,n,t,r){return e.addEventListener(n,t,r)}function o(e,n,t){return e.attachEvent("on"+n,f(e,n,t))}function i(e,n,t,r){return e.removeEventListener(n,t,r)}function u(e,n,t){var r=l(e,n,t);return r?e.detachEvent("on"+n,r):void 0}function c(e,n,t){function r(){var e;return p.createEvent?(e=p.createEvent("Event"),e.initEvent(n,!0,!0)):p.createEventObject&&(e=p.createEventObject()),e}function o(){return new s(n,{detail:t})}var i=-1===v.indexOf(n)?o():r();e.dispatchEvent?e.dispatchEvent(i):e.fireEvent("on"+n,i)}function a(e,n,r){return function(n){var o=n||t.event;o.target=o.target||o.srcElement,o.preventDefault=o.preventDefault||function(){o.returnValue=!1},o.stopPropagation=o.stopPropagation||function(){o.cancelBubble=!0},o.which=o.which||o.keyCode,r.call(e,o)}}function f(e,n,t){var r=l(e,n,t)||a(e,n,t);return h.push({wrapper:r,element:e,type:n,fn:t}),r}function l(e,n,t){var r=d(e,n,t);if(r){var o=h[r].wrapper;return h.splice(r,1),o}}function d(e,n,t){var r,o;for(r=0;r + +{% endblock %} + +{% block widerepo %} +
+
+
+

+ {{ board.name }} +

+
+ {% if g.repo_committer %} +
+ +
+ {% endif %} +
+ +
+ +
+ +
+{% endblock %} + +{% block jscripts %} +{{ super() }} + + + + + +{% endblock %} diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html index 771e563..bb79cf8 100644 --- a/pagure/templates/issue.html +++ b/pagure/templates/issue.html @@ -545,6 +545,33 @@ namespace=repo.namespace, repo=repo.name, issueid=issueid) + {% if issue.boards_issues %} +
+
+
+ Boards + {{ issue.boards_issues |count}} +
+
+
+ {% for board_issue in issue.boards_issues %} +
+
+ {{ board_issue.status.board.name }} + Status: {{ board_issue.status.name }} +
+
+ {% endfor %} +
+
+ {% endif %} + {% if attachments %}
@@ -552,15 +579,6 @@ namespace=repo.namespace, repo=repo.name, issueid=issueid) Attachments {{attachments|count}}
- {##} {{ show_attachments(attachments) }} diff --git a/pagure/templates/repo_master.html b/pagure/templates/repo_master.html index d46652d..cf02914 100644 --- a/pagure/templates/repo_master.html +++ b/pagure/templates/repo_master.html @@ -286,6 +286,8 @@ {% block repo %} {% endblock %} +{% block widerepo %} +{% endblock %} {% endblock %} {% block jscripts %} diff --git a/pagure/templates/repo_master_navbar.html b/pagure/templates/repo_master_navbar.html index fb485e8..a99878b 100644 --- a/pagure/templates/repo_master_navbar.html +++ b/pagure/templates/repo_master_navbar.html @@ -83,6 +83,26 @@ {% endif %} + {% if repo.active_boards %} + + {% endif %} +