aboutsummaryrefslogtreecommitdiffstats
path: root/src/htmlbuilder.h
blob: 3ade530b50ae19573c97760f1dbfca02206e09fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/*
 * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
 * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#pragma once

#include <QColor>
#include <QList>
#include <QPalette>

#include "mimeparser.h"

/// One message's place in a rendered thread.
struct ThreadRenderItem
{
    ParsedMessage message;

    /// Matched messages render in full; unmatched collapse to a one-line stub.
    bool expanded = true;

    /// Disambiguates cid: references. Two newsletters in one thread commonly
    /// use the same Content-ID (cid:logo@example.org), which would collide in
    /// a single document, so every reference is rewritten to
    /// cid:<prefix>!<id>.
    ///
    /// Requirement on whatever generates this value: it must never contain
    /// '!'. The separator that makes cid:<prefix>!<id> unambiguous is the
    /// FIRST '!' in the namespaced string; that only holds if the prefix
    /// half is guaranteed free of the character, since the id half is
    /// attacker-controlled and may legitimately contain '!' itself. The
    /// documented "m<index>" form (e.g. "m0", "m1") satisfies this. Enforced
    /// with Q_ASSERT at both places that perform this concatenation
    /// (HtmlBuilder::namespaceCids and CidSchemeHandler::namespacedKey).
    QString cidPrefix;
};

/// Turns parsed messages into the HTML string handed to the web view.
///
/// Plain text goes through the same path as HTML so the view has one render
/// path rather than two. A whole thread renders as ONE document rather than one
/// view per message: a thread of newsletters can hold dozens of messages, and a
/// QWebEngineView each would spawn a Chromium render process each.
class HtmlBuilder
{
public:
    enum Mode {
        PreferHtml,  ///< Use the HTML part when the message has one.
        ForcePlain,  ///< Always render the plain part, escaped.
    };

    /// The colours the document's own stylesheet uses.
    ///
    /// Passed in rather than read from qApp inside the builder, so the CSS can
    /// be tested against a known palette without a running application, and so
    /// nothing here depends on widget state.
    ///
    /// **Scope.** These style the chrome around messages and the plain-text
    /// render. A message that brings its own HTML brings its own colours, and
    /// those are deliberately left alone: rewriting a sender's styling would
    /// break layouts that depend on it, and a newsletter that sets a white
    /// background is entitled to stay white.
    struct Palette {
        QColor background;  ///< The pane itself.
        QColor text;        ///< Body text.
        QColor dim;         ///< Headers and stubs: present but secondary.
        QColor border;      ///< Rules between messages.
        QColor quote;       ///< Quoted lines in plain text.
    };

    /// The brand colours of the placeholder pane.
    ///
    /// **A deliberate exception to the Palette above**, which derives from the
    /// desktop theme. A logo is brand rather than chrome, so these are the
    /// values from the user's mockup and are not blended toward anything. The
    /// desktop theme still decides WHICH set is used, so the pane never renders
    /// a light lockup on a dark desktop.
    struct BrandPalette {
        QColor background;   ///< The pane, behind the radial wash.
        QColor backgroundIn; ///< The lighter centre of that wash.
        QColor grid;         ///< Grid rules, and the icon tile's border.
        QColor tile;         ///< The icon tile's fill.
        QColor tileBorder;   ///< The icon tile's edge. Separate from `grid`,
                             ///< which needs a different strength on light.
        QColor accent;       ///< "Mail" in the wordmark, and the envelope.
        QColor accentEdge;   ///< The envelope's stroke.
        QColor title;        ///< The wordmark, apart from the accent span.
        QColor subtitle;     ///< The tagline, the helpers and the footer.

        /// Percent alpha of the accent glow. Deliberately different between the
        /// two sets: on dark the glow adds light and can be generous, on light
        /// it subtracts and the same value washes the whole pane.
        int glowAlpha = 0;

        /// Percent opacity of the grid, for the same reason.
        int gridOpacity = 35;
    };

    /// Picks the dark or the light brand set from the desktop palette.
    ///
    /// Decided on the window's Base lightness, the same surface the document
    /// Palette reads, so the two agree about which way round the theme is.
    static BrandPalette brandPaletteFrom(const QPalette &palette);

    /// One helper line under the wordmark: a count, and the query it runs.
    ///
    /// An empty query renders as text rather than as a link, which is what the
    /// sync line uses: it reports a state rather than naming a search.
    struct PlaceholderHelper {
        QString label;  ///< Already-translated, e.g. "12 unread".
        QString query;  ///< notmuch query, or empty for a non-link line.
    };

    /// The pane shown when no thread is displayed.
    ///
    /// Rendered into the same web view as a message rather than into a second
    /// widget stacked behind it, so there is one document path and one set of
    /// security rules.
    static QString buildPlaceholder(const QList<PlaceholderHelper> &helpers,
                                    const QString &version,
                                    const BrandPalette &brand);

    /// Derives the document palette from a widget palette.
    ///
    /// The dim and border colours are blends rather than fixed greys, which is
    /// what makes this work on a dark theme: a hardcoded #555 that reads as
    /// "subtle" on white is nearly invisible on near-black.
    static Palette paletteFrom(const QPalette &palette);

    /// The palette used when a caller supplies none: the running application's.
    /// Falls back to a light default with no QApplication, which only happens
    /// in a test that did not ask for a palette.
    static Palette defaultPalette();

    /// Single message, used for the error card and for tests.
    static QString build(const ParsedMessage &message, Mode mode);
    static QString build(const ParsedMessage &message, Mode mode,
                         const Palette &palette);

    /// The whole thread, oldest first.
    static QString buildThread(const QList<ThreadRenderItem> &items, Mode mode);
    static QString buildThread(const QList<ThreadRenderItem> &items, Mode mode,
                               const Palette &palette);

    /// Rewrites cid: URLs in an HTML body to their namespaced form.
    static QString namespaceCids(const QString &html, const QString &prefix);

private:
    static QString renderPlain(const QString &text);
    static QString renderBody(const ThreadRenderItem &item, Mode mode);
    static QString renderStub(const ParsedMessage &message);
    static QString document(const QString &bodyHtml, const Palette &palette);
    static QString styleSheet(const Palette &palette);
};