Author: Jim Winstead (jimwins)
Committer: GitHub (web-flow)
Pusher: jimwins
Date: 2025-03-24T09:06:28-07:00

Commit: 
https://github.com/php/web-news/commit/0479a8fae8dc955bae1eec3296fd311e4abb438e
Raw diff: 
https://github.com/php/web-news/commit/0479a8fae8dc955bae1eec3296fd311e4abb438e.diff

Handle format=flowed messages (#31)

When the `text/plain` part has the `flowed` format, we can be a little
more clever about how we transform the message into HTML.

 * Switch to non-monospace font and let long lines wrap naturally
 * Handle quotes as blocks with side-border
 * Handle indented and triple-tick code blocks

This also fixes handling of links, including Markdown-style, and adds
handling of inline Markdown-style code blocks

Changed paths:
  M  article.php
  M  lib/fMailbox.php
  M  style.css


Diff:

diff --git a/article.php b/article.php
index 6aa8e28..bc5a34a 100644
--- a/article.php
+++ b/article.php
@@ -122,7 +122,8 @@
 echo "   </table>\n";
 echo "  </blockquote>\n";
 echo "  <blockquote>\n";
-echo "   <pre>\n";
+$class = $mail['flowed'] ? ' class="flowed"' : '';
+echo "   <pre$class>\n";
 
 /*
  * If there was no text part of the message, see what we can do about creating
@@ -149,10 +150,16 @@
 
 $lines = preg_split("@(?<=\r\n|\n)@", $mail['text']);
 $insig = $is_commit = $is_diff = 0;
+$level = 0;
+$in_flow = $was_flowed = false;
+$in_code_block = false;
 
 foreach ($lines as $line) {
+    # Trim end of line
+    $line = preg_replace('/\r?\n$/', '', $line);
+
     # fix lines that started with a period and got escaped
-    if (substr($line, 0, 2) == "..") {
+    if (str_starts_with($line, "..")) {
         $line = substr($line, 1);
     }
 
@@ -161,40 +168,160 @@
         $is_commit = 1;
     }
 
-    # this is some amazingly simplistic code to color quotes/signatures
-    # differently, and turn links into real links. it actually appears
-    # to work fairly well, but could easily be made more sophistimicated.
-    /* NOQUOTES? Why? It creates invalid HTML: http:"x */
-    $line = htmlentities($line, ENT_QUOTES, "utf-8");
+    # We don't use htmlentities() because it seems like overkill and that
+    # makes all of the later substitutions more complicated.
+    $line = htmlspecialchars($line, ENT_QUOTES|ENT_SUBSTITUTE|ENT_HTML5, 
"utf-8");
+
+    # Turn bare links, not within [] or (), to HTML links
+    $line = preg_replace(
+        
"/(^|[^[(])((mailto|https?|ftp|nntp|news):.+?)(&gt;|\\s|\\)|\\.\\s|$)/",
+        "\\1<a href=\"\\2\">\\2</a>\\4",
+        $line
+    );
+
+    # Turn Markdown links to HTML links
     $line = preg_replace(
-        "/((mailto|https?|ftp|nntp|news):.+?)(&gt;|\\s|\\)|\\.\\s|$)/",
-        "<a href=\"\\1\">\\1</a>\\3",
+        "/\[((mailto|https?|ftp|nntp|news):.+?)\]\((.+?)\)/",
+        "<a href=\"\\1\">\\3</a>",
         $line
     );
-    if (!$insig && ($line == "-- \r\n" || $line == "--\r\n")) {
+
+    # Highlight inline code
+    $line = preg_replace(
+        "/`([^`]+?)`/",
+        "<code>\\1</code>",
+        $line
+    );
+
+    # Begin signature when we see the tell-tale '-- ' line
+    if (!$insig && $line == "-- ") {
         echo "<span class=\"signature\">";
         $insig = 1;
     }
+
+    # In commit messages, highlight lines that start with + or -
     if (!$insig && $is_commit && preg_match('/^[-+]/', $line, $m)) {
         $is_diff = 1;
         echo '<span class="' . ($m[0] == '+' ? 'added' : 'removed') . '">';
     }
-    if (!$insig && preg_match('/^((\w*?&gt; ?)+)/', $line, $m)) {
-        $level = substr_count($m[1], '&gt;') % 4;
-        echo "<span class=\"quote$level\">", wordwrap($line, 90, "\n" . 
$m[1]), "</span>";
-    } else {
-        echo wordwrap($line, 90);
+
+    # This gets a little tricker -- "flowed" messages basically have long
+    # quoted lines broken up, so we can put quoted blocks in levels of <div>
+    # blocks instead of highlighting them per-line
+
+    if (!$insig && $mail['flowed']) {
+        $flowed = false;
+        $new_level = 0;
+
+        if (preg_match('/^((\s*&gt;)+)(.*)/', $line, $m)) {
+            $new_level = substr_count($m[1], $m[2]);
+            $line = $m[3];
+        }
+
+        # Trim leading space (a format=flowed thing)
+        if (str_starts_with($line, ' ')) {
+            $line = substr($line, 1);
+        }
+
+        # A "flowed" line ends with a space. We also remove it if DelSp = 
"Yes".
+        if (str_ends_with($line, ' ')) {
+            $flowed = true;
+            if ($mail['delsp']) {
+                $line = substr($line, 0, -1);
+            }
+        }
+
+        # If this line had more quoting, go ahead and open to that level
+        if ($new_level && $new_level > $level) {
+            foreach (range($level + 1, $new_level) as $this_level) {
+                echo "<div class=\"quote quote{$this_level}\">";
+            }
+            $level = $new_level;
+            $in_flow = true;
+        }
+        # Otherwise if we are in a flow, but this line's level is lower (but
+        # not 0), we need to close up the higher levels
+        elseif ($in_flow && $new_level && $new_level < $level) {
+            echo str_repeat('</div>', $level - $new_level);
+            $level = $new_level;
+        }
+
+        # Handle indented code blocks
+        if (preg_match('/( |\xC2\xA0){4}/', $line)) {
+            if (!$in_code_block) {
+                echo '<pre>';
+                $in_code_block = true;
+            }
+        } elseif (!$flowed && !$was_flowed) {
+            if ($in_code_block && is_bool($in_code_block)) {
+                echo '</pre>';
+                $in_code_block = false;
+            }
+        }
+
+        # Handle ``` delimited code blocks
+        if (preg_match('/^```(\w+)?$/', $line, $m)) {
+            if ($in_code_block) {
+                echo '</pre>';
+                $in_code_block = false;
+                continue;
+            } else {
+                $language = $m[1] ?? 'php';
+                echo "<pre class=\"language_{$language}\">";
+                $in_code_block = $language;
+                continue;
+            }
+        }
+
+        # Hey, it's the actual line of text!
+        echo $line;
+
+        # If the line is fixed, we close a flow or just add a newline
+        if (!$flowed) {
+            if ($level != $new_level) {
+                # Close out code block if we were in one
+                if ($in_code_block) {
+                    echo '</pre>';
+                    $in_code_block = false;
+                }
+                echo str_repeat("</div>", $level) . "\n";
+                $level = 0;
+                $in_flow = false;
+            } else {
+                echo "\n";
+            }
+        }
+
+        $was_flowed = $flowed;
+    }
+    # Otherwise we're in a signature or not flowed
+    else {
+        if (!$insig && preg_match('/^((\s*\w*?&gt; ?)+)/', $line, $m)) {
+            $level = substr_count($m[1], '&gt;') % 4;
+            echo "<span class=\"quote$level\">", wordwrap($line, 100, "\n" . 
$m[1]), "</span>";
+        } else {
+            echo wordwrap($line, 100);
+        }
+        echo "\n";
     }
+
+    # If this line was a diff, close out the <span>
     if ($is_diff) {
         $is_diff = 0;
         echo '</span>';
     }
 }
 
+if ($in_code_block) {
+    echo '</pre>';
+}
 if ($insig) {
     echo "</span>";
     $insig = 0;
 }
+if ($mail['flowed'] && $level) {
+    echo str_repeat('</div>', $level);
+}
 
 echo "<br><br>";
 
diff --git a/lib/fMailbox.php b/lib/fMailbox.php
index 60b8afa..a148c48 100644
--- a/lib/fMailbox.php
+++ b/lib/fMailbox.php
@@ -189,8 +189,12 @@ private static function handlePart($info, $structure)
 
 
         $has_disposition = !empty($structure['disposition']);
-        $is_text         = $structure['type'] == 'text' && 
$structure['subtype'] == 'plain';
         $is_html         = $structure['type'] == 'text' && 
$structure['subtype'] == 'html';
+        $is_text         = $structure['type'] == 'text' && 
$structure['subtype'] == 'plain';
+        if ($is_text) {
+            $info['flowed']  = strtolower($structure['type_fields']['format'] 
?? "") == 'flowed';
+            $info['delsp']   = strtolower($structure['type_fields']['delsp'] 
?? "") == 'yes';
+        }
 
         // If the part doesn't have a disposition and is not the default text 
or html, set the disposition to inline
         if (!$has_disposition && ((!$is_text || !empty($info['text'])) && 
(!$is_html || !empty($info['html'])))) {
diff --git a/style.css b/style.css
index d8f91ce..7d0e23e 100644
--- a/style.css
+++ b/style.css
@@ -370,6 +370,29 @@ table.standard th.subr {
 .quote3 { color: #a36008; }
 .quote0 { color: #909; }
 
+pre.flowed {
+    max-width: 100ch;
+    font-family: "Fira Sans", "Source Sans Pro", Helvetica, Arial, sans-serif;
+    font-size: 16px;
+}
+pre.flowed code, pre.flowed pre {
+    font-weight: 700;
+    color: #369;
+}
+pre.flowed pre {
+    background: rgba(0,0,0,0.05);
+    border: 1px solid rgba(0,0,0,0.2);
+    padding: 0.5rem;
+    margin: 0;
+}
+
+div.quote {
+    border-left: 2px solid #777;
+    padding: 0;
+    padding-left: 1em;
+    margin: 0;
+}
+
 /* Highlight of diffs in commit messages */
 .added   { color: #000099; }
 .removed { color: #990000; }

Reply via email to