/ src / views / elements / GitElement.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\views\elements;

use seekquarry\yioop as B;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\CrawlConstants;
use seekquarry\yioop\controllers\components\SocialComponent;
use seekquarry\yioop\models\Model;
use seekquarry\yioop\views\layouts\WebLayout;
use seekquarry\yioop\library\wiki as LW;

/** For tl, getLocaleTag and Yioop constants */
require_once __DIR__.'/../../library/Utility.php';
/**
 * GitElement draws the pages of a git repository a group keeps: the
 * files of the repository itself, its readme, its history and diffs,
 * its statistics, and the issues raised against it with the controls
 * that hold, ban and unban them.
 *
 * The wiki view draws the rest of a group's pages and reaches this
 * element by name through the view helper, in three places: the issue
 * rows a search asks for, the repository itself on a read page, and the
 * statistics on an edit page.
 *
 * @author Chris Pollett
 */
class GitElement extends Element implements CrawlConstants
{
    /**
     * render draws a Git repository page, which is what a bare request
     * to draw means for this element. The method has to exist because
     * every element declares one, and without it the class cannot be
     * built at all, so a repository page died before drawing anything.
     * In practice the wiki element names one of the parts below
     * directly rather than coming through here.
     * @param array $data fields prepared by the controller for whichever
     *     part of the page is being drawn
     */
    public function render($data)
    {
        $this->renderGitRepository($data);
    }
    /**
     * renderGitRepository draws a Git repository wiki page as a able to be
     * looked through file
     * tree. Shows the address to clone from, the branch being viewed,
     * breadcrumb links back up through the folders, and then either the
     * contents of one file or the list of files and folders at the current
     * spot, with any README rendered below the list. Every value shown is
     * prepared and escaped by the controller, so this method only lays them
     * out.
     * @param array $data fields prepared by initializeGitRepositoryReadMode,
     *     among them GIT_CLONE_URL, GIT_BRANCH, GIT_CRUMBS, GIT_ENTRIES,
     *     GIT_FILE, and GIT_README_HTML
     */
    public function renderGitRepository($data)
    {
        $icon_helper = $this->view->helper('iconlink');
        ?>
        <div class="git-repository">
        <?php
        if (!empty($data["GIT_EMPTY"])) { ?>
            <div class="git-clone-line git-open">
            <code class="git-clone-cmd">git clone <?=
                $data["GIT_CLONE_URL"] ?></code>
            <?php $icon_helper->renderFormButton("button", "clone_copy",
                false, "git-icon-control", 'onclick="gitCopyClone(this)" ' .
                'data-clone="' . $data["GIT_CLONE_URL"] . '"'); ?>
            </div>
            <p class="git-empty"><?= tl("wiki_element_git_empty") ?></p>
            </div>
            <?php
            return;
        }
        if (($data["GIT_VIEW"] ?? "") === "issues") {
            $this->renderGitIssues($data);
            ?></div><?php
            return;
        } ?>
        <div class="git-bar">
        <span class="git-branch">
        <select id="git-branch-select" class="git-branch-select"
            onchange="window.location=this.value;"><?php
        foreach ($data["GIT_BRANCH_OPTIONS"] as $option) {
            $chosen = ($option["SELECTED"]) ? " selected='selected'" : "";
            e("<option value='" . $option["URL"] . "'" . $chosen . ">" .
                $option["NAME"] . "</option>");
        }
        ?></select>
        </span>
        <span class="git-ref">
        <button type="button" class="git-ref-button"
            onclick="toggleGitPanel('git-ref-menu')"><?=
            $data["GIT_REF_LABEL"] ?> <span class="git-caret">&#9662;</span>
        </button>
        <div id="git-ref-menu" class="git-ref-menu git-panel"><?php
        foreach ($data["GIT_REF_MENU"] as $item) {
            $item_class = ($item["KIND"] === "head") ? "git-ref-head" :
                "git-ref-item";
            e("<a class='" . $item_class . "' href='" . $item["URL"] .
                "'>" . $item["LABEL"] . "</a>");
        }
        ?></div>
        </span>
        <span class="git-bar-actions">
        <span class="git-download">
        <button type="button" class="git-icon-button"
            onclick="toggleGitPanel('git-download-menu')"
            aria-label="<?= tl("wiki_element_git_download") ?>"><span
            class="git-icon">&#10515;</span></button>
        <div id="git-download-menu" class="git-download-menu git-panel">
        <a href="<?= $data["GIT_DOWNLOAD_TARGZ"] ?>">tar.gz</a>
        <a href="<?= $data["GIT_DOWNLOAD_ZIP"] ?>">zip</a>
        </div>
        </span>
        <button type="button" class="git-icon-button"
            onclick="toggleGitPanel('git-search-line')"
            aria-label="<?= tl("wiki_element_git_search") ?>"><span
            class="git-icon">&#128269;</span></button>
        </span>
        </div>
        <div id="git-search-line" class="git-search-line git-panel<?=
            (!empty($data["GIT_LIST_SEARCH"])) ? " git-open" : "" ?>"><?php
        if (($data["GIT_VIEW"] ?? "") === "commits" ||
            ($data["GIT_VIEW"] ?? "") === "tags") { ?>
            <input type="text" class="git-search-field"
                value="<?= $data["GIT_LIST_SEARCH"] ?? "" ?>"
                placeholder="<?= tl("wiki_element_git_search") ?>"
                onkeydown="gitListSearch(event, '<?=
                    $data["GIT_LIST_SORT_BASE"] ?? "" ?>')">
        <?php } else { ?>
            <input type="text" class="git-search-field"
                placeholder="<?= tl("wiki_element_git_search") ?>"
                oninput="gitTreeFilter(this)">
        <?php } ?>
        </div>
        <?php if (count($data["GIT_CRUMBS"]) > 1) { ?>
        <div class="git-path"><?php
        $crumb_count = count($data["GIT_CRUMBS"]);
        e("<a href='" . $data["GIT_CRUMBS"][0]["URL"] . "'>/</a>");
        $separator = " ";
        foreach (array_slice($data["GIT_CRUMBS"], 1) as $index => $crumb) {
            if ($index === $crumb_count - 2) {
                e($separator . $crumb["NAME"]);
            } else {
                e($separator . "<a href='" . $crumb["URL"] . "'>" .
                    $crumb["NAME"] . "</a>");
            }
            $separator = " / ";
        }
        if (!empty($data["GIT_RESOURCE_DOWNLOAD_URL"])) {
            e("<a class='git-path-download' title='" .
                tl("wiki_element_git_download") . "' href='" .
                $data["GIT_RESOURCE_DOWNLOAD_URL"] . "'>&#10515;</a>");
        }
        ?></div>
        <?php } ?>
        <?php
        if (($data["GIT_VIEW"] ?? "") === "diff") {
            $this->renderGitDiff($data);
        } else if (!empty($data["GIT_VIEW"])) {
            $this->renderGitHistory($data);
        } else if (isset($data["GIT_FILE"]) ||
            !empty($data["GIT_FILE_MEDIA"]) ||
            !empty($data["GIT_FILE_BINARY"]) ||
            !empty($data["GIT_FILE_TOO_LARGE"])) { ?>
            <div class="git-file">
            <?php if (!empty($data["GIT_FILE_MEDIA"])) {
                $media_kind = $data["GIT_FILE_MEDIA_KIND"] ?? "image";
                if ($media_kind === "pdf") { ?>
                    <embed class="git-file-pdf" type="application/pdf"
                        src="<?= $data["GIT_FILE_MEDIA"] ?>">
                <?php } else if ($media_kind === "audio") { ?>
                    <audio class="git-file-audio" controls
                        src="<?= $data["GIT_FILE_MEDIA"] ?>"></audio>
                <?php } else if ($media_kind === "video") { ?>
                    <video class="git-file-video" controls
                        src="<?= $data["GIT_FILE_MEDIA"] ?>"></video>
                <?php } else { ?>
                    <img class="git-file-image" alt=""
                        src="<?= $data["GIT_FILE_MEDIA"] ?>">
                <?php }
            } else if (!empty($data["GIT_FILE_BINARY"])) { ?>
                <p class="git-note"><?= tl("wiki_element_git_binary") ?></p>
            <?php } else if (!empty($data["GIT_FILE_TOO_LARGE"])) { ?>
                <p class="git-note"><?= tl("wiki_element_git_too_large") ?></p>
            <?php } else { ?>
                <pre class="git-blob"><?= $data["GIT_FILE"] ?></pre>
            <?php } ?>
            </div>
            <?php
        } else { ?>
            <div class="git-listing-box">
            <table class="git-listing" id="git-listing">
            <tr><th onclick="gitTableSort(this)"><div class="resizable"><?=
                tl("wiki_element_git_name") ?></div></th>
                <th class="git-hide-narrow" onclick="gitTableSort(this)"><div
                class="resizable"><?=
                tl("wiki_element_git_message") ?></div></th>
                <th class="git-hide-narrow" onclick="gitTableSort(this)"><div
                class="resizable"><?=
                tl("wiki_element_git_author") ?></div></th>
                <th data-sort-type="number" onclick="gitTableSort(this)"><?=
                tl("wiki_element_git_age") ?></th></tr>
            <?php
            if (!empty($data["GIT_PARENT_URL"])) {
                e("<tr class='git-no-sort'><td class='git-name-col'><a href='"
                    . $data["GIT_PARENT_URL"] . "'>&#8593; " .
                    tl("wiki_element_parent_folder") . "</a></td>" .
                    "<td class='git-commit-col git-hide-narrow'></td>" .
                    "<td class='git-author-col git-hide-narrow'></td>" .
                    "<td class='git-age-col'></td></tr>");
            }
            $time_formatter = $this->view->helper('timeformatter');
            foreach ($data["GIT_ENTRIES"] as $entry) {
                $suffix = ($entry["IS_DIR"]) ? "/" : "";
                $commit_time = $entry["COMMIT_TIME"] ?? 0;
                e("<tr><td class='git-name-col'><a href='" .
                    $entry["URL"] . "'>" . $entry["NAME"] . $suffix .
                    "</a></td>" .
                    "<td class='git-commit-col git-hide-narrow'>" .
                    ($entry["COMMIT_SUMMARY"] ?? "") . "</td>" .
                    "<td class='git-author-col git-hide-narrow'>" .
                    ($entry["COMMIT_AUTHOR"] ?? "") . "</td>" .
                    "<td class='git-age-col' data-sort-key='" .
                    $commit_time . "'>" .
                    $time_formatter->timeAgo($commit_time) .
                    "</td></tr>");
            } ?>
            </table>
            </div>
            <?php if (!empty($data["GIT_README_HTML"])) {
                $this->renderGitReadme($data);
            } ?>
        <?php } ?>
        </div><?php
    }
    /**
     * renderHeldGitIssues draws the reports waiting for an editor: one line
     * each, with a box to pick it out, the handle its reporter typed in, the
     * short token standing for where it came from, its summary and when it
     * arrived. The whole of what was written is not shown here, because a queue
     * is for picking out of; the summary links through to the report laid out
     * the way an issue is. Any number of reports can be picked and decided
     * together, which is what makes a queue of bogus reports quick to clear,
     * and rejecting them can shut their reporters out for a while.
     * @param array $data fields set up by the controller, holding this page of
     *     waiting reports and the spans a reporter may be shut out for
     */
    private function renderHeldGitIssues($data)
    {
        if (!empty($data["GIT_ISSUE_HELD_DETAIL"])) {
            $this->renderHeldGitIssueDetail($data);
            return;
        }
        $reports = $data["GIT_ISSUE_HELD_ROWS"];
        if (empty($reports)) {
            ?><p class="git-note"><?=
                tl("wiki_element_git_no_held_issues") ?></p><?php
            $this->renderGitBannedLink($data);
            return;
        }
        $time_formatter = $this->view->helper('timeformatter');
        ?>
        <form method="post" id="git-held-form"
            action="<?= $data["GIT_ISSUES_URL"] ?>">
        <input type="hidden" name="<?= C\p('CSRF_TOKEN') ?>"
            value="<?= $data["GIT_ISSUE_TOKEN"] ?>">
        <input type="hidden" name="repo_issue_held" id="git-held-picked"
            value="">
        <?php
            /* Which decision was made normally arrives as the value of the
               button pressed. Pressing delete presses no button, so it puts
               the decision here instead; a button pressed comes later in
               the form and so has the last word. */
            ?>
        <input type="hidden" name="repo_issue_held_action"
            id="git-held-action" value="">
        <div class="git-issue-list-box">
        <table class="git-issue-list git-held-list" id="git-held-list">
        <tr><th><div class="resizable"><?=
            tl("wiki_element_git_issue_reporter") ?></div></th>
        <th><div class="resizable"><?=
            tl("wiki_element_git_issue_summary") ?></div></th>
        <th class="git-hide-narrow"><?=
            tl("wiki_element_git_issue_updated") ?></th></tr>
        <?php
        $this->renderHeldGitIssueRows($data); ?>
        </table>
        </div>
        <div class="git-issue-held-decide">
        <button type="submit" name="repo_issue_held_action" value="accept"
            class="git-issue-decide-button"><?=
            tl("wiki_element_git_accept_issue") ?></button>
        <?php $this->renderGitDiscardControl(); ?>
        </div>
        </form>
        <script>
        window.addEventListener("load", function() {
            initHeldSelection("git-held-form",
                "<?= tl('wiki_element_git_discard_confirm') ?>");
        });
        </script>
        <?php
        $this->renderGitScrollPager($data, "git-held-list");
        $this->renderGitBannedLink($data);
    }
    /**
     * renderGitBannedLink draws the way through to the list of reporters shut
     * out of this repository, under everything else and in the middle, as a
     * line of text rather than a button: it leads somewhere rather than doing
     * something.
     * @param array $data fields set up by the controller, holding the address
     *     the list is at
     */
    private function renderGitBannedLink($data)
    {
        if (empty($data["GIT_ISSUE_BAN_URL"])) {
            return;
        }
        ?>
        <div class="git-issue-banned-link">
        <a href="<?= $data["GIT_ISSUE_BAN_URL"] ?>"><?=
            tl("wiki_element_git_view_banned") ?></a>
        </div><?php
    }
    /**
     * renderGitIssueBans draws the reporters shut out of this repository: who
     * they are, the token standing for where they reported from, and when they
     * may report again. Each row carries one control for letting them back in,
     * with the other lengths of shutting out under its catch, and a search
     * above narrows the list by name or token.
     * @param array $data fields set up by the controller, holding this page of
     *     the list, what was searched for, and the way back
     */
    private function renderGitIssueBans($data)
    {
        $rows = $data["GIT_ISSUE_BAN_ROWS"];
        ?>
        <div class="git-issue-detail-head">
        <a class="git-issue-back" href="<?= $data["GIT_ISSUE_HELD_URL"] ?>"
            >&laquo; <?= tl("wiki_element_git_issue_back") ?></a>
        </div>
        <div class="git-ban-search">
        <input type="search" name="repo_ban_search"
            value="<?= $data["GIT_ISSUE_BAN_SEARCH"] ?>"
            placeholder="<?= tl("wiki_element_git_ban_search") ?>"
            onkeydown="gitListSearch(event, '<?=
                $data["GIT_ISSUE_BAN_BASE"] ?>', 'repo_ban_search')">
        </div>
        <?php if (empty($rows)) { ?>
        <p class="git-note"><?= tl("wiki_element_git_no_banned") ?></p>
        <?php } else { ?>
        <div class="git-issue-list-box">
        <table class="git-issue-list" id="git-ban-list">
        <tr><th><?= tl("wiki_element_git_issue_reporter") ?></th>
        <th><?= tl("wiki_element_git_banned_until") ?></th>
        <th><?= tl("wiki_element_git_banned_action") ?></th></tr>
        <?php
        $this->renderGitIssueBanRows($data); ?>
        </table>
        </div>
        <?php }
        $this->renderGitScrollPager($data, "git-ban-list");
    }
    /**
     * renderGitIssueBanRows draws the rows of the list of reporters shut out,
     * the same whether they are the first page or a further one fetched as the
     * list is scrolled.
     * @param array $data fields set up by the controller, holding the records
     *     of the page being drawn
     */
    private function renderGitIssueBanRows($data)
    {
        foreach ($data["GIT_ISSUE_BAN_ROWS"] as $row) {
            $handle = ($row["HANDLE"] === "") ?
                tl("wiki_element_git_anon_no_handle") : $row["HANDLE"];
            $until = ($row["UNTIL"] == C\FOREVER) ?
                tl("wiki_element_git_banned_forever") :
                date("Y-m-d", $row["UNTIL"]);
            ?><tr class="git-issue-row">
            <td class="git-issue-held-who"><?= $handle ?>
            <span class="git-issue-held-token"><?=
                htmlentities($row["TOKEN"]) ?></span></td>
            <td><?= $until ?></td>
            <td><?php $this->renderGitUnbanControl($data,
                $row["TOKEN"]); ?></td>
            </tr><?php
        }
    }
    /**
     * renderGitUnbanControl draws the control for a reporter who is shut out:
     * letting them back in, and under its catch the other lengths they could be
     * shut out for instead.
     * @param array $data fields set up by the controller, holding the address
     *     to post to and what was searched for
     * @param string $token the token standing for the reporter
     */
    private function renderGitUnbanControl($data, $token)
    {
        $choices = [
            "ban_week" => tl("wiki_element_git_ban_week"),
            "ban_month" => tl("wiki_element_git_ban_month"),
            "ban_forever" => tl("wiki_element_git_ban_forever"),
        ];
        ?>
        <form method="post" class="git-discard-split"
            action="<?= $data["GIT_ISSUES_URL"] ?>">
        <input type="hidden" name="<?= C\p('CSRF_TOKEN') ?>"
            value="<?= $data["GIT_ISSUE_TOKEN"] ?>">
        <input type="hidden" name="repo_ban_token"
            value="<?= htmlentities($token) ?>">
        <input type="hidden" name="repo_ban_search"
            value="<?= $data["GIT_ISSUE_BAN_SEARCH"] ?>">
        <button type="submit" name="repo_ban_action" value="unban"
            class="git-issue-decide-button git-discard-main"><?=
            tl("wiki_element_git_unban") ?></button>
        <button type="button" class="git-issue-decide-button
            git-discard-catch" aria-haspopup="true" aria-expanded="false"
            title="<?= tl("wiki_element_git_ban_change") ?>"
            onclick="gitToggleDiscardMenu(this)">&#9662;</button>
        <span class="git-discard-menu">
        <?php foreach ($choices as $value => $label) {
            e("<button type='submit' name='repo_ban_action' value='" .
                $value . "' class='git-discard-choice'>" . $label .
                "</button>");
        } ?>
        </span>
        </form><?php
    }
    /**
     * renderGitDiscardControl draws the control for throwing reports away: a
     * plain discard, and at its end a catch holding the same thing done with a
     * shutting out of the reporter for a week, a month, or for good. They are
     * one control because they are one decision made with different weight, and
     * which weight was chosen arrives as the name of the decision itself.
     */
    private function renderGitDiscardControl()
    {
        $choices = [
            "discard_week" => tl("wiki_element_git_discard_week"),
            "discard_month" => tl("wiki_element_git_discard_month"),
            "discard_forever" => tl("wiki_element_git_discard_forever"),
        ];
        ?>
        <span class="git-discard-split">
        <button type="submit" name="repo_issue_held_action" value="discard"
            class="git-issue-decide-button git-discard-main"><?=
            tl("wiki_element_git_discard_issue") ?></button>
        <button type="button" class="git-issue-decide-button
            git-discard-catch" aria-haspopup="true" aria-expanded="false"
            title="<?= tl("wiki_element_git_discard_more") ?>"
            onclick="gitToggleDiscardMenu(this)">&#9662;</button>
        <span class="git-discard-menu">
        <?php foreach ($choices as $value => $label) {
            e("<button type='submit' name='repo_issue_held_action' " .
                "value='" . $value . "' class='git-discard-choice'>" .
                $label . "</button>");
        } ?>
        </span>
        </span><?php
    }
    /**
     * renderGitIssueRowsOnly chooses which list's rows to draw for a request
     * asking for a further page of one.
     * The wiki element calls this while drawing a Git page, so it is
     * reachable from outside this class.
     * @param array $data fields set up by the controller, holding the rows of
     *     the page asked for
     */
    public function renderGitIssueRowsOnly($data)
    {
        if (isset($data["GIT_ISSUE_BAN_ROWS"])) {
            $this->renderGitIssueBanRows($data);
            return;
        }
        if (isset($data["GIT_ISSUE_HELD_ROWS"])) {
            $this->renderHeldGitIssueRows($data);
            return;
        }
        $this->renderGitIssueListRows($data);
    }
    /**
     * renderGitScrollPager sets a list going as one that asks for its next page
     * when it has been scrolled near its end, rather than being turned a page
     * at a time. Nothing is drawn here: the box the list sits in is what
     * scrolls, and the rows that arrive go into the table inside it.
     * @param array $data fields set up by the controller, holding how many rows
     *     there are altogether, how many are on a page, how far into the list
     *     this page starts, and the address a page is asked for under
     * @param string $table_id id of the table the rows are added to
     */
    private function renderGitScrollPager($data, $table_id)
    {
        $total = (int)($data["TOTAL_ROWS"] ?? 0);
        $per_page = (int)($data["RESULTS_PER_PAGE"] ?? 1);
        $start = (int)($data["LIMIT"] ?? 0);
        if ($start + $per_page >= $total) {
            return;
        }
        ?>
        <script>
        window.addEventListener("load", function() {
            initGitScrollPager("<?= $table_id ?>", "<?=
                $data["GIT_ISSUE_PAGE_URL"] ?>", <?= $start + $per_page ?>,
                <?= $total ?>, <?= $per_page ?>);
        });
        </script><?php
    }
    /**
     * renderHeldGitIssueRows draws the rows of the queue of waiting reports,
     * the same whether they are the first page or a further one fetched as the
     * list is scrolled.
     * @param array $data fields set up by the controller, holding the reports
     *     of the page being drawn
     */
    private function renderHeldGitIssueRows($data)
    {
        $time_formatter = $this->view->helper('timeformatter');
        foreach ($data["GIT_ISSUE_HELD_ROWS"] as $report) {
            $handle = $report["handle"] ?? "";
            if ($handle === "") {
                $handle = tl("wiki_element_git_anon_no_handle");
            }
            $reported_at = (int)($report["REPORTED"] ?? 0);
            if ($reported_at > 0 && (time() - $reported_at) < C\ONE_DAY) {
                $when = $time_formatter->timeAgo($reported_at);
            } else {
                $when = date("Y-m-d", $reported_at);
            }
            e("<tr class='git-issue-row' data-held-number='" .
                $report["NUMBER"] . "'>" .
                "<td class='git-issue-held-who'>" . htmlentities($handle) .
                "<span class='git-issue-held-token'>" .
                htmlentities($report["reporter_token"] ?? "") .
                "</span></td>" .
                "<td class='git-issue-summary-col'><a href='" .
                $report["URL"] . "'>" . ($report["title"] ?? "") .
                "</a></td>" .
                "<td class='git-hide-narrow'>" . $when . "</td></tr>");
        }
    }
    /**
     * renderHeldGitIssueDetail draws one waiting report laid out the way a
     * reported issue is, so the whole of what was written can be read before
     * deciding on it. Under it sit the editor's two decisions: accepting gives
     * the report an issue number and puts it in the list, throwing it away
     * removes it for good.
     * @param array $data fields set up by the controller, holding the one
     *     report being looked at and the address to post a decision to
     */
    private function renderHeldGitIssueDetail($data)
    {
        $report = $data["GIT_ISSUE_HELD_DETAIL"];
        $handle = $report["handle"] ?? "";
        if ($handle === "") {
            $handle = tl("wiki_element_git_anon_no_handle");
        }
        ?>
        <div class="git-issues git-issue-detail">
        <div class="git-issue-detail-head">
        <a class="git-issue-back" href="<?= $data["GIT_ISSUE_HELD_URL"] ?>"
            >&laquo; <?= tl("wiki_element_git_issue_back") ?></a>
        <span class="git-issue-detail-number"><?=
            tl("wiki_element_git_issue_held") ?></span>
        </div>
        <div class="git-issue-detail-title-row">
        <h3 class="git-issue-detail-title"><?=
            htmlentities($report["title"] ?? "") ?></h3>
        </div>
        <div class="git-issue-comments">
        <div class="git-issue-comment">
        <div class="git-issue-comment-head">
        <span class="git-issue-comment-who"><?=
            htmlentities($handle) ?></span>
        <span class="git-issue-held-token"><?=
            htmlentities($report["reporter_token"] ?? "") ?></span>
        </div>
        <div class="git-issue-comment-body git-issue-held-body"><?=
            htmlentities($report["description"] ?? "") ?></div>
        </div>
        </div>
        <form method="post" class="git-issue-held-decide"
            action="<?= $data["GIT_ISSUES_URL"] ?>">
        <input type="hidden" name="<?= C\p('CSRF_TOKEN') ?>"
            value="<?= $data["GIT_ISSUE_TOKEN"] ?>">
        <input type="hidden" name="repo_issue_held"
            value="<?= $data["GIT_ISSUE_HELD_POSITION"] ?>">
        <button type="submit" name="repo_issue_held_action" value="accept"
            class="git-issue-decide-button"><?=
            tl("wiki_element_git_accept_issue") ?></button>
        <?php $this->renderGitDiscardControl(); ?>
        </form>
        </div><?php
    }
    /**
     * renderGitIssues draws the issue view of a Git repository page. Along the
     * top a search box stretches across the activity, ending in a magnifying-
     * glass button and a funnel button that drops down the choices for
     * narrowing the list: unassigned, open, closed, or the visitor's own
     * issues. For a visitor allowed to report one there is also a beetle button
     * that shows or hides the new-issue form. That form reads like a small
     * editor, its fields hinted by placeholders rather than labels, and it lets
     * the reporter pick the branch and the version the issue was seen on but
     * not its urgency. Beneath comes the list of issues, each a bordered row
     * with a narrow number, a title, a status, and when it was last touched.
     * @param array $data view data holding the prepared issue rows, the branch
     *     and version choices, and, for a visitor who may report one, the
     *     form's security token
     */
    private function renderGitIssues($data)
    {
        if (!empty($data["GIT_ISSUE_DETAIL"])) {
            $this->renderGitIssueDetail($data);
            return;
        }
        $icon_helper = $this->view->helper('iconlink');
        $can_report = !empty($data["GIT_ISSUE_CAN_REPORT"]);
        $filter_url = $data["GIT_ISSUE_FILTER_URL"] ?? "";
        ?>
        <div class="git-issues">
        <div class="git-issue-bar">
        <?php
        $active_filter = $data["GIT_ISSUE_FILTER"] ?? "open";
        $filter_active = ($active_filter !== "all") ?
            " git-issue-filter-active" : "";
        $filter_options = [
            "all" => tl("wiki_element_git_issue_all"),
            "open" => tl("wiki_element_git_issue_open_issues"),
            "closed" => tl("wiki_element_git_issue_closed_issues"),
            "reported" => tl("wiki_element_git_issue_reported"),
            "assigned" => tl("wiki_element_git_issue_assigned"),
            "marked_fixed" => tl("wiki_element_git_issue_marked_fixed"),
            "marked_wont_fix" =>
                tl("wiki_element_git_issue_marked_wont_fix"),
            "mine" => tl("wiki_element_git_issue_mine"),
        ];
        /* Only someone who may edit the repository page is offered the
           queue of reports waiting to be accepted, since only they can act
           on one. */
        if (!empty($data["GIT_ISSUE_CAN_MODERATE"])) {
            $filter_options["held"] = tl("wiki_element_git_issue_held") .
                " (" . ($data["GIT_ISSUE_HELD_COUNT"] ?? 0) . ")";
        }
        ?>
        <span class="git-issue-search-box">
        <input type="text" id="git-issue-search" class="git-issue-search"
            placeholder="<?= tl("wiki_element_git_issue_search") ?>"
            oninput="gitIssueFilter(this)">
        <?php $icon_helper->renderFormButton("button", "search", false,
            "icon-button-container git-issue-search-button",
            "onclick=\"gitIssueFilter(elt('git-issue-search'))\""); ?>
        <span class="git-issue-filter-wrap">
        <?php $icon_helper->renderFormButton("button", "filter", false,
            "icon-button-container git-issue-filter-button" . $filter_active,
            "onclick=\"toggleGitPanel('git-issue-filter-menu')\""); ?>
        <div id="git-issue-filter-menu"
            class="git-panel git-issue-filter-menu">
        <?php foreach ($filter_options as $filter_key => $filter_label) {
            $mark = ($filter_key === $active_filter) ?
                "<span class='git-issue-filter-check'>&#10003;</span> " : "";
            e("<a href='" . $filter_url . $filter_key . "'>" . $mark .
                $filter_label . "</a>");
        } ?>
        </div>
        </span>
        </span>
        <?php if ($can_report) {
            $icon_helper->renderFormButton("button", "new_issue", false,
                "icon-button-container git-issue-new-button",
                "onclick=\"toggleGitPanel('git-issue-form')\"");
        } ?>
        </div>
        <div class="git-issue-search-hint"><?=
            tl("wiki_element_git_issue_search_hint") ?></div>
        <?php if ($can_report) { ?>
        <div id="git-issue-form" class="git-panel git-issue-form">
        <form method="post" action="<?= $data["GIT_ISSUES_URL"] ?>">
        <input type="hidden" name="<?= C\p('CSRF_TOKEN') ?>"
            value="<?= $data["GIT_ISSUE_TOKEN"] ?>">
        <?php if (!empty($data["GIT_ISSUE_ANONYMOUS"])) { ?>
        <input type="text" class="git-issue-title" name="issue_handle"
            maxlength="<?= C\GIT_ISSUE_HANDLE_LEN ?>"
            placeholder="<?= tl("wiki_element_git_issue_handle") ?>">
        <?php
            /* A decoy field, put out of sight and out of the reading order
               the same way the wiki forms put theirs, so that only a
               machine filling in every input gives itself away. It is
               moved off screen rather than hidden outright, since a
               form-filler can be told to skip what is hidden. */
            ?>
        <div aria-hidden="true" style="position:absolute;left:-9999px;
            top:-9999px;width:1px;height:1px;overflow:hidden;">
        <input type="text" tabindex="-1" autocomplete="off"
            name="<?= $data["GIT_ISSUE_HONEYPOT"] ?>" value="">
        </div>
        <input type="hidden" name="nonce_for_string" id="nonce_for_string">
        <input type="hidden" name="random_string" id="random_string"
            value="<?= $data["GIT_ISSUE_PROOF_STRING"] ?? "" ?>">
        <input type="hidden" name="time" id="time"
            value="<?= $data["GIT_ISSUE_PROOF_TIME"] ?? "" ?>">
        <input type="hidden" name="level" id="level"
            value="<?= $data["GIT_ISSUE_PROOF_LEVEL"] ?? "" ?>">
        <?php } ?>
        <input type="text" class="git-issue-title" name="issue_title"
            placeholder="<?= tl("wiki_element_git_issue_title") ?>">
        <textarea class="git-issue-body" name="issue_description"
            placeholder="<?=
            tl("wiki_element_git_issue_description") ?>"></textarea>
        <div class="git-issue-form-foot">
        <select name="issue_branch" class="git-issue-select"
            title="<?= tl("wiki_element_git_issue_branch") ?>">
        <?php foreach ($data["GIT_ISSUE_BRANCHES"] as $branch_name) {
            $selected = ($branch_name ===
                ($data["GIT_ISSUE_DEFAULT_BRANCH"] ?? "")) ?
                " selected='selected'" : "";
            e("<option value='" . $branch_name . "'" . $selected . ">" .
                $branch_name . "</option>");
        } ?>
        </select>
        <select name="issue_version" class="git-issue-select"
            title="<?= tl("wiki_element_git_issue_version") ?>">
        <option value="current"><?=
            tl("wiki_element_git_issue_current") ?></option>
        <?php foreach ($data["GIT_ISSUE_VERSIONS"] as $version_name) {
            e("<option value='" . $version_name . "'>" . $version_name .
                "</option>");
        } ?>
        </select>
        <button type="submit" class="git-issue-submit"><?=
            tl("wiki_element_git_issue_new") ?></button>
        </div>
        </form>
        </div>
        <?php } ?>
        <?php if (isset($data["GIT_ISSUE_BAN_ROWS"])) {
            $this->renderGitIssueBans($data);
        } else if (isset($data["GIT_ISSUE_HELD_ROWS"])) {
            $this->renderHeldGitIssues($data);
        } else if (empty($data["GIT_ISSUE_ROWS"])) { ?>
        <p class="git-note"><?= tl("wiki_element_git_no_issues") ?></p>
        <?php } else { ?>
        <div class="git-issue-list-box">
        <table class="git-issue-list" id="git-issue-list">
        <tr><th class="git-issue-num-col" data-sort-type="number"
                onclick="gitTableSort(this)"><div class="resizable"><?=
                tl("wiki_element_git_issue_number") ?></div></th>
            <th onclick="gitTableSort(this)"><div class="resizable"><?=
                tl("wiki_element_git_issue_priority") ?></div></th>
            <th onclick="gitTableSort(this)"><div class="resizable"><?=
                tl("wiki_element_git_issue_summary") ?></div></th>
            <th onclick="gitTableSort(this)"><div class="resizable"><?=
                tl("wiki_element_git_issue_status") ?></div></th>
            <th class="git-hide-narrow"
                onclick="gitTableSort(this)"><div class="resizable"><?=
                tl("wiki_element_git_issue_user") ?></div></th>
            <th class="git-hide-narrow"
                onclick="gitTableSort(this)"><?=
                tl("wiki_element_git_issue_updated") ?></th></tr>
        <?php
        $this->renderGitIssueListRows($data); ?>
        </table>
        </div>
        <?php $this->renderGitScrollPager($data, "git-issue-list"); ?>
        <?php } ?>
        </div><?php
    }
    /**
     * renderGitIssueListRows draws the rows of the issue list, the same whether
     * they are the first page drawn with the rest of the view or a further page
     * fetched as the list is scrolled.
     * @param array $data fields set up by the controller, holding the rows of
     *     the page being drawn
     */
    private function renderGitIssueListRows($data)
    {
        $time_formatter = $this->view->helper('timeformatter');
        foreach ($data["GIT_ISSUE_ROWS"] as $row) {
            $status_word = $this->gitIssueStatusWord($row["STATUS"]);
            $updated_time = $row["UPDATED_TIME"];
            if ($updated_time > 0 &&
                (time() - $updated_time) < C\ONE_DAY) {
                $updated = $time_formatter->timeAgo($updated_time);
            } else {
                $updated = date("Y-m-d", $updated_time);
            }
            e("<tr class='git-issue-row' data-number='" . $row["NUMBER"] .
                "' data-reporter='" . $row["REPORTER"] .
                "' data-assignee='" . $row["ASSIGNEE"] .
                "' data-status='" . $status_word .
                "' data-updated='" . date("Y-m-d", $row["UPDATED_TIME"]) .
                "'>" .
                "<td class='git-issue-num-col'><a href='" . $row["URL"] .
                "'>#" . $row["NUMBER"] . "</a></td>" .
                "<td class='git-issue-priority-col'>" .
                $this->gitIssuePriorityMarkup($row["PRIORITY"]) . "</td>" .
                "<td class='git-issue-summary-col'><a href='" .
                $row["URL"] . "'>" . $row["TITLE"] . "</a></td>" .
                "<td>" . $status_word . "</td>" .
                "<td class='git-hide-narrow'>" .
                $row["STATUS_USER"] . "</td>" .
                "<td class='git-hide-narrow'>" . $updated .
                "</td></tr>");
        }
    }
    /**
     * gitIssueStatusWord gives the status of an issue in words for the status
     * column and for the read-only status line: reported, assigned, marked
     * fixed, or marked won't fix. Words are used rather than a bare colored
     * mark so the meaning is plain.
     * @param string $status one of the four display-status names
     * @return string the status in words
     */
    private function gitIssueStatusWord($status)
    {
        if ($status === LW\WikiIssue::DISPLAY_ASSIGNED) {
            return tl("wiki_element_git_issue_assigned");
        }
        if ($status === LW\WikiIssue::DISPLAY_FIXED) {
            return tl("wiki_element_git_issue_marked_fixed");
        }
        if ($status === LW\WikiIssue::DISPLAY_WONT_FIX) {
            return tl("wiki_element_git_issue_marked_wont_fix");
        }
        return tl("wiki_element_git_issue_reported");
    }
    /**
     * gitIssuePriorityMarkup draws the priority of an issue as a small marked
     * shape for the issue list: one exclamation in a circle for low, two in a
     * triangle for medium, and three in a stop sign for high, so urgency reads
     * at a glance and takes little width on a narrow screen. An unknown
     * priority draws nothing.
     * @param int $priority the issue's priority, low, medium, or high
     * @return string the marked-shape HTML, or an empty string
     */
    private function gitIssuePriorityMarkup($priority)
    {
        $level = (int)$priority;
        $marks = [LW\WikiIssue::PRIORITY_LOW => "!",
            LW\WikiIssue::PRIORITY_MEDIUM => "!!",
            LW\WikiIssue::PRIORITY_HIGH => "!!!"];
        $shapes = [LW\WikiIssue::PRIORITY_LOW => "git-priority-low",
            LW\WikiIssue::PRIORITY_MEDIUM => "git-priority-medium",
            LW\WikiIssue::PRIORITY_HIGH => "git-priority-high"];
        if (!isset($marks[$level])) {
            return "";
        }
        $labels = [LW\WikiIssue::PRIORITY_LOW =>
            tl("wiki_element_git_priority_low"),
            LW\WikiIssue::PRIORITY_MEDIUM =>
            tl("wiki_element_git_priority_medium"),
            LW\WikiIssue::PRIORITY_HIGH =>
            tl("wiki_element_git_priority_high")];
        return "<span class='git-priority " . $shapes[$level] .
            "' title='" . $labels[$level] . "'>" . $marks[$level] .
            "</span>";
    }
    /**
     * renderGitIssueDetail draws the detail page for one issue. At the top a
     * back link returns to the list, followed by the issue number. Then comes
     * the dated list of status changes it has been through, each naming who
     * made it. A group editor gets a status control set to the current status:
     * choosing assigned reveals a username box and choosing marked fixed
     * reveals a commit box, each with an arrow to send it, while the other
     * choices send at once; a reader who cannot edit just sees the status in
     * words. The priority follows the same way, then the title and description.
     * @param array $data view data holding the prepared issue detail
     */
    private function renderGitIssueDetail($data)
    {
        $can_edit = !empty($data["GIT_ISSUE_CAN_EDIT"]);
        $status = $data["GIT_ISSUE_DISPLAY_STATUS"];
        $priority = (int)$data["GIT_ISSUE_PRIORITY"];
        ?>
        <div class="git-issues git-issue-detail">
        <div class="git-issue-detail-head">
        <a class="git-issue-back"
            href="<?= $data["GIT_ISSUES_URL"] ?>">&laquo;
            <?= tl("wiki_element_git_issue_back") ?></a>
        <span class="git-issue-detail-number"><?=
            tl("wiki_element_git_issue_number") ?>
            #<?= $data["GIT_ISSUE_NUMBER"] ?></span>
        </div>
        <div class="git-issue-detail-info">
        <ul class="git-issue-history">
        <?php
        /* The first line of an issue's history says when it was reported
           and by whom, and so stands for the issue itself; the way to
           throw the issue away sits on that line, where someone allowed
           to edit will look for it. */
        $is_first_line = true;
        foreach ($data["GIT_ISSUE_HISTORY"] as $line) {
            $delete_markup = "";
            if ($is_first_line && !empty($data["GIT_ISSUE_DELETE_URL"])) {
                $delete_markup = " " .
                    $this->view->helper("iconlink")->renderButton(
                    $data["GIT_ISSUE_DELETE_URL"], "delete", "", true);
            }
            e("<li><span class='git-issue-history-when'>" . $line["WHEN"] .
                "</span> " . $line["LABEL"] . $delete_markup . "</li>");
            $is_first_line = false;
        } ?>
        </ul>
        <?php if ($can_edit) { ?>
        <form class="git-issue-change" method="post"
            action="<?= $data["GIT_ISSUE_DETAIL_URL"] ?>">
        <input type="hidden" name="<?= C\p('CSRF_TOKEN') ?>"
            value="<?= $data["GIT_ISSUE_TOKEN"] ?>">
        <label class="git-issue-detail-label"><?=
            tl("wiki_element_git_issue_status") ?>:</label>
        <select name="issue_action" class="git-issue-select"
            onchange="gitIssueStatusChange(this)">
        <option value="reported"<?=
            ($status === LW\WikiIssue::DISPLAY_REPORTED) ?
            " selected='selected'" : "" ?>><?=
            tl("wiki_element_git_issue_reported") ?></option>
        <option value="assigned"<?=
            ($status === LW\WikiIssue::DISPLAY_ASSIGNED) ?
            " selected='selected'" : "" ?>><?=
            tl("wiki_element_git_issue_assigned") ?></option>
        <option value="marked_fixed"<?=
            ($status === LW\WikiIssue::DISPLAY_FIXED) ?
            " selected='selected'" : "" ?>><?=
            tl("wiki_element_git_issue_marked_fixed") ?></option>
        <option value="marked_wont_fix"<?=
            ($status === LW\WikiIssue::DISPLAY_WONT_FIX) ?
            " selected='selected'" : "" ?>><?=
            tl("wiki_element_git_issue_marked_wont_fix") ?></option>
        </select>
        <span id="git-issue-assignee-group" class="git-issue-change-field">
        <input type="text" name="issue_assignee"
            placeholder="<?= tl("wiki_element_git_issue_username") ?>">
        <button type="submit" class="git-issue-arrow"
            title="<?= tl("wiki_element_git_issue_apply") ?>">&#8594;</button>
        </span>
        <span id="git-issue-commit-group" class="git-issue-change-field">
        <input type="text" name="issue_commit"
            placeholder="<?= tl("wiki_element_git_issue_commit") ?>">
        <button type="submit" class="git-issue-arrow"
            title="<?= tl("wiki_element_git_issue_apply") ?>">&#8594;</button>
        </span>
        </form>
        <form class="git-issue-priority-form" method="post"
            action="<?= $data["GIT_ISSUE_DETAIL_URL"] ?>">
        <input type="hidden" name="<?= C\p('CSRF_TOKEN') ?>"
            value="<?= $data["GIT_ISSUE_TOKEN"] ?>">
        <label class="git-issue-detail-label"><?=
            tl("wiki_element_git_issue_priority") ?>:</label>
        <select name="issue_priority" class="git-issue-select"
            onchange="this.form.submit()">
        <option value="1"<?= ($priority == 1) ?
            " selected='selected'" : "" ?>><?=
            tl("wiki_element_git_issue_low") ?></option>
        <option value="2"<?= ($priority == 2) ?
            " selected='selected'" : "" ?>><?=
            tl("wiki_element_git_issue_medium") ?></option>
        <option value="3"<?= ($priority == 3) ?
            " selected='selected'" : "" ?>><?=
            tl("wiki_element_git_issue_high") ?></option>
        </select>
        </form>
        <?php } else { ?>
        <div class="git-issue-detail-line"><?=
            tl("wiki_element_git_issue_status") ?>:
            <?= $this->gitIssueStatusWord($status) ?></div>
        <div class="git-issue-detail-line"><?=
            tl("wiki_element_git_issue_priority") ?>: <?php
        if ($priority >= 3) {
            e(tl("wiki_element_git_issue_high"));
        } else if ($priority == 2) {
            e(tl("wiki_element_git_issue_medium"));
        } else {
            e(tl("wiki_element_git_issue_low"));
        } ?></div>
        <?php } ?>
        <div class="git-issue-detail-title-row">
        <h3 class="git-issue-detail-title"><?=
            $data["GIT_ISSUE_TITLE"] ?></h3>
        <span class="git-issue-comment-controls">
        <button type="button" class="git-issue-comment-latest"
            title="<?= tl("wiki_element_git_issue_latest") ?>"
            onclick="gitIssueScrollComments()">&#8595;</button>
        <?php if (!empty($data["GIT_ISSUE_CAN_COMMENT"])) { ?>
        <button type="button" class="git-issue-comment-add"
            title="<?= tl("wiki_element_git_issue_add_comment") ?>"
            onclick="toggleDisplay('add-comment')">&#128172;</button>
        <?php } ?>
        </span>
        </div>
        <div class="git-issue-comments" id="git-issue-comments">
        <?php foreach ($data["GIT_ISSUE_COMMENTS"] as $comment) {
            e("<div class='git-issue-comment'>" .
                "<div class='git-issue-comment-head'>" .
                "<span class='git-issue-comment-who'>" . $comment["NAME"] .
                "</span> <span class='git-issue-comment-when'>" .
                $comment["WHEN"] . "</span></div>" .
                "<div class='git-issue-comment-body'>" . $comment["BODY"] .
                "</div></div>");
        } ?>
        </div>
        <?php if (!empty($data["GIT_ISSUE_CAN_COMMENT"])) { ?>
        <div id="add-comment" class="git-issue-comment-form">
        <form method="post" action="<?= $data["GIT_ISSUE_DETAIL_URL"] ?>">
        <input type="hidden" name="<?= C\p('CSRF_TOKEN') ?>"
            value="<?= $data["GIT_ISSUE_TOKEN"] ?>">
        <textarea id="comment-add-comment" name="description"
            class="medium-text-area"
            data-buttons="all,!wikibtn-search,!wikibtn-heading,!wikibtn-slide"
            ></textarea>
        <script>
        document.write('<div class="upload-box">' +
            '<input type="file" id="file-add-comment" ' +
            'name="file_add-comment" class="none" multiple="multiple" >' +
            '<?= tl("groupfeed_element_drag_textarea") ?>' +
            '<a href="javascript:elt(\'file-add-comment\').click()"><?=
            tl("groupfeed_element_click_textarea") ?></a></div>');
        </script>
        <button class="button-box float-opposite" type="submit"><?=
            tl("wiki_element_git_issue_comment_send") ?></button>
        <div>&nbsp;<br><br></div>
        </form>
        </div>
        <?php } ?>
        </div>
        </div><?php
    }
    /**
     * renderGitStatistics draws the repository statistics beneath a Git page's
     * file listing: a line of totals followed by small bar charts of the
     * busiest authors, the commits made each month, and the commonest file
     * endings. Nothing is drawn when a page has no statistics, such as an empty
     * repository.
     * The wiki element calls this while drawing a Git page, so it is
     * reachable from outside this class.
     * @param array $data view data holding the prepared statistics
     */
    public function renderGitStatistics($data)
    {
        if (!isset($data["GIT_STATS_COMMITS"])) {
            return;
        }
        ?><div class="git-stats">
        <h2><?= tl("wiki_element_git_statistics") ?></h2>
        <p class="git-stats-totals"><?=
            tl("wiki_element_git_stats_totals", $data["GIT_STATS_COMMITS"],
            $data["GIT_STATS_FILES"]) ?></p>
        <?php
        $this->renderGitStatGroup(tl("wiki_element_git_stats_authors"),
            $data["GIT_STATS_AUTHORS"], "authors");
        $this->renderGitStatGroup(tl("wiki_element_git_stats_months"),
            $data["GIT_STATS_MONTHS"], "months");
        $this->renderGitStatGroup(tl("wiki_element_git_stats_types"),
            $data["GIT_STATS_TYPES"], "types");
        ?>
        </div><?php
    }
    /**
     * renderGitStatGroup draws one titled group of statistic bars. Each row
     * shows its label, a bar whose width is already worked out as a percentage,
     * and the count. Every row is drawn; the rows sit in a box tall enough for
     * a useful number of them (set per group in the style sheet by the group
     * key) and the box scrolls when there are more, so a long group does not
     * fill the page yet nothing is hidden behind a control.
     * @param string $title heading for the group, already localized
     * @param array $bars rows to draw, each with a label, a count, and a bar
     *     width
     * @param string $key short name unique to this group, used to pick the box
     *     height in the style sheet
     */
    private function renderGitStatGroup($title, $bars, $key)
    {
        if (empty($bars)) {
            return;
        }
        ?><div class="git-stats-group">
        <h3><?= $title ?></h3>
        <div class="git-stats-rows git-stats-<?= $key ?>">
        <?php foreach ($bars as $bar) {
            $this->renderGitStatRow($bar);
        } ?>
        </div>
        </div><?php
    }
    /**
     * renderGitStatRow draws a single statistic row: its label, the bar whose
     * width is already worked out as a percentage, and the count. A file ending
     * that is empty is shown with a readable stand-in name.
     * @param array $bar the row to draw, with a label, a count, and a bar width
     */
    private function renderGitStatRow($bar)
    {
        $label = ($bar["label"] === "") ?
            tl("wiki_element_git_no_ending") : $bar["label"];
        ?><div class="git-stat-row">
        <span class="git-stat-label"><?= $label ?></span>
        <span class="git-stat-track"><span class="git-stat-bar"
            style="width: <?= $bar["width"] ?>%"></span></span>
        <span class="git-stat-count"><?= $bar["count"] ?></span>
        </div><?php
    }
    /**
     * renderGitHistory draws the commit list or the tag list in place of the
     * file listing. A header row names the columns, the first page of rows is
     * placed in the table body, and a small script wires the box so that
     * scrolling near its bottom fetches and appends the next page. The rows and
     * the address to fetch further pages from are prepared and escaped by the
     * controller.
     * @param array $data view data holding GIT_VIEW, GIT_LIST_ROWS,
     *     GIT_LIST_SCROLL_URL, and GIT_LIST_PAGE_SIZE
     */
    private function renderGitHistory($data)
    {
        $is_tags = ($data["GIT_VIEW"] === "tags");
        e('<div id="git-list-box" class="git-listing-box">');
        e('<table class="git-listing git-history"><thead><tr>');
        if ($is_tags) {
            e($this->gitSortHeader($data, "tag",
                tl("wiki_element_git_tag"), "git-name-col"));
            e($this->gitSortHeader($data, "date",
                tl("wiki_element_git_date"), "git-date-col"));
        } else {
            e($this->gitSortHeader($data, "date",
                tl("wiki_element_git_date"), "git-date-col"));
            e($this->gitSortHeader($data, "author",
                tl("wiki_element_git_author"), "git-author-col"));
        }
        e($this->gitSortHeader($data, "message",
            tl("wiki_element_git_message"), "git-msg-col"));
        e('<th class="git-actions-col">' .
            tl("wiki_element_git_actions") . '</th></tr></thead>');
        e('<tbody id="git-list-rows">' . $data["GIT_LIST_ROWS"] .
            '</tbody></table></div>');

    }
    /**
     * gitSortHeader builds one able to be clicked column header for the
     * commit or tag
     * list. The header links back to the same list re-sorted by this column; if
     * the list is already sorted by it, the link flips the direction and an
     * arrow shows which way it currently runs. The link address is the escaped
     * base the controller prepared, so this only assembles it.
     * @param array $data view data holding GIT_LIST_SORT_BASE and the current
     *     GIT_LIST_SORT and GIT_LIST_DIR
     * @param string $column the column key to sort by ("date", "author",
     *     "message", or "tag")
     * @param string $label the heading text to show
     * @param string $css_class the column's cell class
     * @return string the header cell as HTML
     */
    private function gitSortHeader($data, $column, $label, $css_class)
    {
        $base = $data["GIT_LIST_SORT_BASE"] ?? "";
        $current = $data["GIT_LIST_SORT"] ?? "";
        $direction = $data["GIT_LIST_DIR"] ?? "desc";
        $next = ($current === $column && $direction === "asc") ?
            "desc" : "asc";
        $arrow = "";
        if ($current === $column) {
            $arrow = ($direction === "asc") ? " &#9650;" : " &#9660;";
        }
        return '<th class="' . $css_class . '"><a class="git-sort" ' .
            'href="' . $base . "&repo_sort=" . $column . "&repo_dir=" .
            $next . '">' . $label . $arrow . '</a></th>';
    }
    /**
     * renderGitDiff draws one commit's changes: its message and author, then
     * each changed file with a line by line comparison of its earlier and later
     * contents. Every value is prepared and escaped by the controller, so this
     * method only lays them out.
     * @param array $data view data holding GIT_DIFF_SUBJECT, GIT_DIFF_AUTHOR,
     *     GIT_DIFF_FILES, and GIT_DIFF_EMPTY
     */
    private function renderGitDiff($data)
    {
        ?>
        <div class="git-diff">
        <p class="git-diff-head"><?= $data["GIT_DIFF_SUBJECT"] ?>
        <span class="git-diff-author"><?=
            $data["GIT_DIFF_AUTHOR"] ?></span></p>
        <?php if (!empty($data["GIT_DIFF_EMPTY"])) { ?>
            <p class="git-note"><?=
                tl("wiki_element_git_no_changes") ?></p>
        <?php } else {
            e($data["GIT_DIFF_FILES"]);
        } ?>
        </div><?php
    }
    /**
     * renderGitReadme draws the README box that sits under a Git repository's
     * file list: a bar carrying a book icon and the word README, an optional
     * contents button that opens the list of the README's own section headings
     * so a reader can jump to one, and then the rendered README itself. The
     * heading list and the rendered README are prepared and escaped by the
     * controller, so this method only lays them out.
     * @param array $data view data holding GIT_README_HTML and, when the README
     *     has more than one heading, GIT_README_TOC
     */
    private function renderGitReadme($data)
    {
        $book = '<span class="git-readme-icon">&#128214;</span>';
        $contents = '<span class="git-readme-icon">&#9776;</span>';
        e('<div class="git-readme-box"><div class="git-readme-bar">');
        e('<span class="git-readme-title">' . $book . ' ' .
            tl("wiki_element_git_readme") . '</span>');
        if (!empty($data["GIT_README_TOC"])) {
            e('<button type="button" class="git-readme-toc-toggle" ' .
                'onclick="toggleGitPanel(\'git-readme-toc\')" ' .
                'aria-label="' .
                tl("wiki_element_git_contents") . '">' . $contents .
                '</button>');
        }
        e('</div>');
        if (!empty($data["GIT_README_TOC"])) {
            e('<ul id="git-readme-toc" class="git-readme-toc git-panel">');
            foreach ($data["GIT_README_TOC"] as $item) {
                e('<li class="git-toc-level' . $item["LEVEL"] . '">' .
                    '<a href="' . $item["URL"] . '">' . $item["TEXT"] .
                    '</a></li>');
            }
            e('</ul>');
        }
        e('<div class="git-readme">' . $data["GIT_README_HTML"] . '</div>');
        e('</div>');
    }
}
X