<?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">▾</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">⤓</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">🔍</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"] . "'>⤓</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"] . "'>↑ " .
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"] ?>"
>« <?= 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)">▾</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)">▾</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"] ?>"
>« <?= 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'>✓</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"] ?>">«
<?= 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") ?>">→</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") ?>">→</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()">↓</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')">💬</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> <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") ? " ▲" : " ▼";
}
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">📖</span>';
$contents = '<span class="git-readme-icon">☰</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>');
}
}