Author: Jim Winstead (jimwins)
Committer: GitHub (web-flow)
Pusher: jimwins
Date: 2025-04-06T12:25:53-07:00
Commit: Display thread tree as a tree (#33) · php/web-news@b882172 · GitHub
Raw diff: https://github.com/php/web-news/commit/b882172274ee4d32db0cca6838acbc3da2451fb8.diff
Display thread tree as a tree (#33)
Changed paths:
M article.php
M lib/ThreadTree.php
M lib/common.php
M style.css
Diff:
diff --git a/article.php b/article.php
index bc5a34a..7db4dbd 100644
--- a/article.php
+++ b/article.php
@@ -371,21 +371,7 @@
<h2>
Thread (<?= sprintf("%d message%s", $count = $threads->count(), $count > 1 ? 's' : '') ?>)
</h2>
- <div class="responsive-table">
- <table class="standard">
- <thead>
- <tr>
- <th>#</th>
- <th>Subject</th>
- <th>Author</th>
- <th>Date</th>
- </tr>
- </thead>
- <tbody>
- <?php $threads->printRows($group, 'utf8'); ?>
- </tbody>
- </table>
- </div>
+ <?php $threads->printFullThread($group, $article, charset: 'utf8'); ?>
</blockquote>
<?php
} catch (\Throwable $t) {
diff --git a/lib/ThreadTree.php b/lib/ThreadTree.php
index f9c09c3..06e4434 100644
--- a/lib/ThreadTree.php
+++ b/lib/ThreadTree.php
@@ -94,4 +94,101 @@ public function printRows($group, $charset = 'utf8')
$this->printArticleAndChildren($root, $group, $charset, 1);
}
}
+
+ public function printFullThread(
+ $group,
+ $includingArticleNumber,
+ $charset = null
+ ) {
+ echo "<div class=\"list-tree\"><ul>";
+ $this->printThread(
+ group: $group,
+ messageId: $this->root,
+ activeArticleNumber: $includingArticleNumber,
+ charset: $charset,
+ );
+
+ foreach ($this->extraRootChildren as $childMessageId) {
+ $this->printThread(
+ group: $group,
+ activeArticleNumber: $includingArticleNumber,
+ messageId: $childMessageId,
+ charset: $charset,
+ );
+ }
+
+ echo "</ul></div>";
+ }
+
+ public function printThread(
+ $group,
+ $messageId = null,
+ $activeArticleNumber = null,
+ $depth = 0,
+ $subject = "",
+ $charset = 'utf8'
+ ) {
+ if ($depth > 40) {
+ echo "<li>Too deep!</li>";
+ return;
+ }
+
+ if (array_key_exists($messageId, $this->articleNumbers)) {
+ $articleNumber = $this->articleNumbers[$messageId];
+
+ # for debugging that we've actually handled all articles
+ #unset($this->articleNumbers[$messageId]);
+
+ $details = $this->articles[$articleNumber];
+
+ echo '<li>';
+
+ $details = $this->articles[$articleNumber];
+
+ if ($articleNumber != $activeArticleNumber) {
+ echo "<a href=\"/$group/$articleNumber\">";
+ } else {
+ echo "<b>";
+ }
+ echo
+ '<span class="author">',
+ format_author($details['author'], $charset, nameOnly: true),
+ '</span>',
+ '<span class="date">',
+ '<time datetime="', format_date($details['date'], 'c'), '">',
+ format_date($details['date']),
+ '</time>',
+ '</span>';
+
+ $newSubject = format_subject($details['subject'], $charset, trimRe: true);
+ if ($messageId != $this->root && $newSubject != $subject) {
+ echo '<span class="subject">';
+ echo format_subject($details['subject'], $charset);
+ echo '</span>';
+ }
+
+ if ($articleNumber != $activeArticleNumber) {
+ echo "</a>";
+ } else {
+ echo "</b>";
+ }
+
+ if (array_key_exists($messageId, $this->tree)) {
+ echo '<ul>';
+ foreach ($this->tree[$messageId] as $childMessageId) {
+ $this->printThread(
+ group: $group,
+ activeArticleNumber: $activeArticleNumber,
+ messageId: $childMessageId,
+ subject: $newSubject,
+ charset: $charset,
+ depth: $depth + 1,
+ );
+ }
+ echo '</ul>';
+ }
+
+ echo "</li>";
+ }
+ }
}
diff --git a/lib/common.php b/lib/common.php
index 3e7577c..8c680fc 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -238,36 +238,39 @@ function spam_protect($txt)
# this turns some common forms of email addresses into mailto: links
-function format_author($a, $charset = 'iso-8859-1')
+function format_author($a, $charset = 'iso-8859-1', $nameOnly = false)
{
$a = recode_header($a, $charset);
if (preg_match("/^\s*(.+)\s+\\(\"?(.+?)\"?\\)\s*$/", $a, $ar)) {
- return "<a href=\"mailto:" .
- htmlspecialchars(urlencode(spam_protect($ar[1])), ENT_QUOTES, "UTF-8") .
- "\" class=\"email fn n\">" .
- str_replace(" ", " ", htmlspecialchars($ar[2], ENT_QUOTES, "UTF-8")) . "</a>";
+ $email= spam_protect($ar[1]);
+ $name = $ar[2];
}
- if (preg_match("/^\s*\"?(.+?)\"?\s*<(.+)>\s*$/", $a, $ar)) {
+ elseif (preg_match("/^\s*\"?(.+?)\"?\s*<(.+)>\s*$/", $a, $ar)) {
+ $email = spam_protect($ar[2]);
+ $name = $ar[1];
+ }
+ elseif (strpos("@", $a) !== false) {
+ $email = $name = spam_protect($a);
+ } else {
+ $email = $name = $a;
+ }
+ if ($nameOnly) {
+ return str_replace(" ", " ", htmlspecialchars($name, ENT_QUOTES, "UTF-8"));
+ } else {
return "<a href=\"mailto:" .
- htmlspecialchars(urlencode(spam_protect($ar[2])), ENT_QUOTES, "UTF-8") .
+ htmlspecialchars(urlencode($email), ENT_QUOTES, "UTF-8") .
"\" class=\"email fn n\">" .
- str_replace(" ", " ", htmlspecialchars($ar[1], ENT_QUOTES, "UTF-8")) . "</a>";
- }
- if (strpos("@", $a) !== false) {
- $a = spam_protect($a);
- return "<a href=\"mailto:" . htmlspecialchars(urlencode($a), ENT_QUOTES, "UTF-8") .
- "\" class=\"email fn n\">" . htmlspecialchars($a, ENT_QUOTES, "UTF-8") . "</a>";
+ str_replace(" ", " ", $name) . "</a>";
}
- return str_replace(" ", " ", htmlspecialchars($a, ENT_QUOTES, "UTF-8"));
}
-function format_subject($s, $charset = 'iso-8859-1')
+function format_subject($s, $charset = 'iso-8859-1', $trimRe = false)
{
global $article;
$s = recode_header($s, $charset);
/* Trim most of the prefixes we add for lists */
- $s = preg_replace('/^(Re:\s*)?(\s*\[(DOC|PEAR|PECL|PHP|ANNOUNCE|GIT-PULLS|STANDARDS|php-standards)(-.+?)?]\s*)+/', '\1', $s);
+ $s = preg_replace('/^(Re:\s*)?(\s*\[(DOC|PEAR|PECL|PHP|ANNOUNCE|GIT-PULLS|STANDARDS|php-standards)(-.+?)?]\s*(Re:\s*)?)+/', $trimRe ? '' : '\1\5', $s);
// make this look better on the preview page..
if (strlen($s) > 150 && !isset($article)) {
@@ -279,11 +282,11 @@ function format_subject($s, $charset = 'iso-8859-1')
}
-function format_title($s, $charset = 'iso-8859-1')
+function format_title($s, $charset = 'iso-8859-1', $trimRe = false)
{
global $article;
$s = recode_header($s, $charset);
- $s = preg_replace("/^(Re: *)?\[(PHP|PEAR)(-.*?)?\] /i", "\\1", $s);
+ $s = preg_replace("/^(Re:\s*)?\[(PHP|PEAR)(-.*?)?\]\s/i", $trimRe ? "" : "\\1", $s);
// make this look better on the preview page..
if (strlen($s) > 150 && !isset($article)) {
$s = substr($s, 0, 150) . "...";
@@ -293,10 +296,10 @@ function format_title($s, $charset = 'iso-8859-1')
return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
}
-function format_date($d)
+function format_date($d, $format = 'r')
{
$d = strtotime($d);
- $d = gmdate('r', $d);
+ $d = gmdate($format, $d);
return str_replace(" ", " ", $d);
}
diff --git a/style.css b/style.css
index 7d0e23e..cdec4c5 100644
--- a/style.css
+++ b/style.css
@@ -410,6 +410,81 @@ form.subscription-form {
gap: 1em;
}
+/* Thread tree, based on: Pure CSS Tree View: Simple & Unlimited Nesting | CSS Script */
+.list-tree {
+ --tree-clr: #075985;
+ --tree-font-size: 1rem;
+ --tree-item-height: 1.5;
+ --tree-offset: 0.5rem;
+ --tree-indent: 0.5rem;
+ --tree-thickness: 1px;
+ --tree-style: solid;
+}
+.list-tree ul{
+ display: grid;
+ list-style: none;
+ font-size: var(--tree-font-size);
+ padding-inline-start: var(--tree-indent);
+ max-width: 50em;
+}
+.list-tree li{
+ line-height: var(--tree-item-height);
+ padding-inline-start: var(--tree-offset);
+ border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr);
+ position: relative;
+ text-indent: .5rem;
+
+ &:last-child {
+ border-color: transparent; /* hide (not remove!) border on last li element*/
+ }
+
+ & a, & b {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-item: start;
+ & span.author {
+ grid-column: 1 / 1;
+ white-space: normal;
+ }
+ & span.date {
+ grid-column: 2 / 2;
+ white-space: nowrap;
+ font-variant-numeric: tabular-nums;
+ }
+ & span.subject {
+ grid-column: 1 / 2;
+ white-space: normal;
+ }
+ }
+ &::before{
+ content: '';
+ position: absolute;
+ top: calc(var(--tree-font-size) / 2 + var(--tree-item-height) / 2 * -1 * var(--tree-font-size) + var(--tree-thickness));
+ left: calc(var(--tree-thickness) * -1);
+ width: calc(var(--tree-offset) + var(--tree-thickness) * 2);
+ height: calc(var(--tree-item-height) * var(--tree-font-size) - var(--tree-font-size) / 2);
+ border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr);
+ border-bottom: var(--tree-thickness) var(--tree-style) var(--tree-clr);
+ }
+ &::after{
+ content: '';
+ position: absolute;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: var(--tree-clr);
+ top: calc(var(--tree-item-height) / 2 * 1rem);
+ left: var(--tree-offset) ;
+ translate: calc(var(--tree-thickness) * -1) calc(var(--tree-thickness) * -1);
+ }
+ & li li{
+ /*
+ change line color etc.
+ --tree-clr: rgb(175, 208, 84);
+ */
+ }
+}
+
@media screen and (max-width: 760px) {
.welcome {
display: none;