1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "fmt"
7 "strings"
8 "unicode"
9)
10
11// invisible reports whether r is a character that changes how code reads
12// without being seen: the bidirectional controls and the other format
13// characters (Cf: zero-width, joiners, tags, BOM…), the control characters,
14// the line and paragraph separators, and the blank Hangul fillers. A tab is
15// not one, nor is the line break of a commit message.
16func invisible(r rune) bool {
17 switch r {
18 case '\t', '\n':
19 return false
20 case 0x115F, 0x1160, 0x3164, 0xFFA0: // Hangul fillers render blank
21 return true
22 }
23 return unicode.IsControl(r) || unicode.Is(unicode.Cf, r) || unicode.Is(unicode.Zl, r) || unicode.Is(unicode.Zp, r)
24}
25
26// marker is the visible form of an invisible character.
27func marker(r rune) string { return fmt.Sprintf("<U+%04X>", r) }
28
29// who is the name shown for the author of a merge request or a comment, or the
30// actor of a run. ermite has one user: the owner's web session, root and every
31// stored token act for the owner, so they show the owner's name (ERMITE_OWNER),
32// not the label of the token, which names a device ("laptop"). Only CI is
33// someone else, shown as stored: a job token ("ci job") and the workflows a job
34// dispatched ("github-actions[bot]").
35func (s *Server) who(stored string) string {
36 if strings.HasPrefix(stored, "ci ") || strings.HasSuffix(stored, "[bot]") {
37 return visibleText(stored)
38 }
39 if s.cfg.Owner != "" {
40 return s.cfg.Owner
41 }
42 return "owner"
43}
44
45// visibleText is s with every invisible character replaced by its marker,
46// for the places that show text as a plain string: paths, names, subjects.
47func visibleText(s string) string {
48 if !strings.ContainsFunc(s, invisible) {
49 return s
50 }
51 var b strings.Builder
52 for _, r := range s {
53 if invisible(r) {
54 b.WriteString(marker(r))
55 } else {
56 b.WriteRune(r)
57 }
58 }
59 return b.String()
60}