Author: Jim Winstead (jimwins)
Committer: GitHub (web-flow)
Pusher: jimwins
Date: 2025-03-24T09:06:28-07:00
Commit: Handle format=flowed messages (#31) · php/web-news@0479a8f · GitHub
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):.+?)(>|\\s|\\)|\\.\\s|$)/",
+ "\\1<a href=\"\\2\">\\2</a>\\4",
+ $line
+ );
+
+ # Turn Markdown links to HTML links
$line = preg_replace(
- "/((mailto|https?|ftp|nntp|news):.+?)(>|\\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*?> ?)+)/', $line, $m)) {
- $level = substr_count($m[1], '>') % 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*>)+)(.*)/', $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*?> ?)+)/', $line, $m)) {
+ $level = substr_count($m[1], '>') % 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; }