Author: Jim Winstead (jimwins) Committer: GitHub (web-flow) Pusher: jimwins Date: 2025-04-06T12:25:53-07:00
Commit: https://github.com/php/web-news/commit/b882172274ee4d32db0cca6838acbc3da2451fb8 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: https://www.cssscript.com/tree-view-unlimited-nesting/ */ +.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;