package markup
import (
"bytes"
"fmt"
"html/template"
"strings"
)
// EmailDoc is everything the quarry email card shows. Body is trusted:
// it is EmailHTML's output, or markup the caller wrote and escaped.
type EmailDoc struct {
Title string
// Mark and Name are the header's left: what the email came from. Both
// empty is quarry's own (account mail). Surface is the header's
// muted right.
Mark template.HTML
Name string
Surface string
Context template.HTML // who did what, where
Meta string // muted line under Context: time and reference
Body template.HTML
Open string // the button's label and target; none without both
OpenURL string
ReplyIn string // what a reply does ("answer in chat"); "" for no hint
Why string
SettingsURL string // "" for mail that has no preference
HomeURL string
HomeName string
// LogoURL is the forge's Q logo, a PNG with its own dark tile: before
// the word QUARRY in application mail's header and in every footer.
LogoURL string
// WordmarkBase is the URL prefix of the wordmark PNGs ("<base>dark-ink.png",
// "light-ink.png" and their "-sm" footer sizes). Empty falls back to the
// word as text alone.
WordmarkBase string
// Brand replaces Name in the header: the wordmark.
Brand template.HTML
// FooterWord is the small wordmark set, built by EmailDocument.
FooterWord template.HTML
}
// wordmarkFont is the application's face, Iosevka Fixed semibold, loaded
// from a public CDN where the client honours it, with condensed system
// faces behind it. The word is text, not an image, so a client that
// recolours a dark message recolours it too.
const wordmarkFont = "'Iosevka Fixed','Iosevka','Arial Narrow','Roboto Condensed','Helvetica Neue Condensed',sans-serif"
// wordmarkSet is the word QUARRY three ways, one of which a client shows:
// the dark-ink image by default (for light backgrounds), the light-ink
// image under a dark scheme, and the styled text only in Gmail, which
// recolours text it forces dark but never images. Hidden copies use
// display:none, so they are out of the accessibility tree and one
// reading of "QUARRY" reaches a screen reader. The rules that swap them
// are in the document's <style>; Gmail is found with `u + .body`, which
// needs the DOCTYPE (Gmail turns it into a <u>) and a classed <body>.
func wordmarkSet(base string, small bool, w, h, text int) template.HTML {
hidden := "display:none;max-height:0;overflow:hidden;mso-hide:all;"
suffix := ".png"
if small {
suffix = "-sm.png"
}
word := fmt.Sprintf(`<span class="q-word q-wm-t" style="%sfont-family:%s;font-size:%dpx;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#282828;">QUARRY</span>`, hidden, wordmarkFont, text)
if base == "" {
return template.HTML(strings.Replace(word, hidden, "", 1))
}
// The image is taller than the cap height by the Q's tail, which drops
// below the baseline; the margin above it keeps the caps where they
// were centred before the tail was included.
tail := 7
if small {
tail = 4
}
img := func(class, ink, style string) string {
return fmt.Sprintf(`<img class="%s" src="%s" alt="QUARRY" width="%d" height="%d" style="%smargin-top:%dpx;width:%dpx;height:%dpx;border:0;">`,
class, template.HTMLEscapeString(base+ink+suffix), w, h, style, tail, w, h)
}
return template.HTML(img("q-wm-d", "dark-ink", "display:block;") + img("q-wm-l", "light-ink", hidden) + word)
}
const mailFont = "'IBM Plex Sans',-apple-system,'Segoe UI',Helvetica,Arial,sans-serif"
var emailDocTmpl = template.Must(template.New("email").Parse(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/iosevka/600.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600;700&family=IBM+Plex+Mono&display=swap">
<title>{{.Title}}</title>
<style>
@media (prefers-color-scheme: dark) {
.q-page { background:#141617 !important; }
.q-card { background:#1d2021 !important; border-color:#504945 !important; }
.q-text, .q-text td, .q-text li, .q-text p { color:#f0ece4 !important; }
.q-muted { color:#bdb6ab !important; }
.q-rule { border-color:#504945 !important; }
.q-a { color:#d99058 !important; }
.q-code { background:#32302f !important; color:#f0ece4 !important; }
.q-th { background:#2a2826 !important; color:#f0ece4 !important; border-color:#504945 !important; }
.q-tde { background:#232526 !important; }
.q-tbl { border-color:#504945 !important; }
.q-quote { border-color:#665c54 !important; color:#bdb6ab !important; }
.q-btn { background:#d99058 !important; color:#1d2021 !important; }
.q-mark { color:#d99058 !important; }
.q-word { color:#f0ece4 !important; }
.q-wm-d { display:none !important; }
.q-wm-l { display:block !important; max-height:none !important; overflow:visible !important; }
.q-idnbg { background:#32302f !important; }
}
[data-ogsc] .q-wm-d { display:none !important; }
[data-ogsc] .q-wm-l { display:block !important; max-height:none !important; overflow:visible !important; }
/* Gmail only: it forces text dark but not images, so it gets the text. */
u + .body .q-wm-d, u + .body .q-wm-l { display:none !important; }
u + .body .q-wm-t { display:inline !important; max-height:none !important; overflow:visible !important; }
</style>
</head>
<body class="body" style="margin:0;padding:0;">
<table role="presentation" class="q-page" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f4;padding:24px 12px;">
<tr><td align="center">
<table role="presentation" class="q-card" width="100%" cellpadding="0" cellspacing="0" style="max-width:600px;background:#fffcf5;border:1px solid #d5c4a1;border-radius:10px;">
<tr><td class="q-rule" style="padding:18px 24px 14px;border-bottom:1px solid #e6dcc0;font-family:` + mailFont + `;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td style="vertical-align:middle;padding-right:8px;line-height:0;font-size:0;">{{.Mark}}</td>
<td class="q-text" style="vertical-align:middle;font-size:16px;font-weight:700;letter-spacing:-0.01em;color:#282828;">{{if .Brand}}{{.Brand}}{{else}}{{.Name}}{{end}}</td>
</tr></table></td>
<td align="right" class="q-muted" style="font-size:12px;color:#665c54;">{{.Surface}}</td>
</tr></table>
</td></tr>
<tr><td style="padding:18px 24px 4px;font-family:` + mailFont + `;">
<div class="q-text" style="font-size:15px;color:#282828;">{{.Context}}</div>{{if .Meta}}
<div class="q-muted" style="font-size:12px;color:#665c54;margin-top:2px;">{{.Meta}}</div>{{end}}
</td></tr>
{{if .Body}} <tr><td class="q-text" style="padding:12px 24px 8px;font-family:` + mailFont + `;font-size:15px;line-height:1.55;color:#282828;">
{{.Body}}
</td></tr>{{end}}{{if and .Open .OpenURL}}
<tr><td style="padding:6px 24px 22px;font-family:` + mailFont + `;">
<a class="q-btn" href="{{.OpenURL}}" style="display:inline-block;background:#a04412;color:#fffcf5;text-decoration:none;font-weight:600;font-size:14px;padding:9px 16px;border-radius:6px;">{{.Open}}</a>{{if .ReplyIn}}
<span class="q-muted" style="font-size:13px;color:#665c54;"> or reply to this email to {{.ReplyIn}}</span>{{end}}
</td></tr>{{end}}
<tr><td class="q-rule q-muted" style="padding:14px 24px 18px;border-top:1px solid #e6dcc0;font-family:` + mailFont + `;font-size:12px;line-height:1.5;color:#665c54;">
{{.Why}}{{if .SettingsURL}}
<a class="q-a" href="{{.SettingsURL}}" style="color:#a04412;">Notification settings</a>{{end}}
<table role="presentation" cellpadding="0" cellspacing="0" style="margin-top:8px;"><tr>
{{if .LogoURL}}<td style="vertical-align:middle;padding-right:6px;line-height:0;font-size:0;"><img src="{{.LogoURL}}" alt="" width="16" height="16" style="display:block;width:16px;height:16px;border:0;border-radius:4px;"></td>{{end}}
<td style="vertical-align:middle;padding-right:5px;line-height:0;font-size:0;">{{.FooterWord}}</td>{{if .HomeURL}}
<td class="q-muted" style="vertical-align:middle;font-size:12px;line-height:1.5;color:#665c54;">·
<a class="q-a" href="{{.HomeURL}}" style="color:#a04412;">{{.HomeName}}</a></td>{{end}}
</tr></table>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
`))
// EmailDocument renders the card around d.Body. ok is false when the
// template fails or the document exceeds EmailHTMLMax; the caller then
// sends text only.
func EmailDocument(d EmailDoc) (string, bool) {
if d.Mark == "" && d.Name == "" {
// The application, not a repository: its Q logo and its name.
if d.LogoURL != "" {
d.Mark = AvatarMark(d.LogoURL, "")
}
d.Brand = wordmarkSet(d.WordmarkBase, false, 120, 37, 20)
}
d.FooterWord = wordmarkSet(d.WordmarkBase, true, 61, 19, 12)
var buf bytes.Buffer
if err := emailDocTmpl.Execute(&buf, d); err != nil || buf.Len() > EmailHTMLMax {
return "", false
}
return buf.String(), true
}