package markup
import (
"fmt"
"regexp"
"strings"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
// LandingRef is what a LandingDescriber knows about the landing a !N
// names: its number and where it stands.
type LandingRef struct {
Number int // the number the reader sees
State string // "open", "accepted" or "rejected"
}
// LandingDescriber is an OPTIONAL upgrade on Resolver: a !N that names a
// landing reads as that landing's state icon and number.
type LandingDescriber interface {
DescribeLanding(t RefToken) (LandingRef, bool)
}
// refIcon is the glyph a resolved reference wears in front of its
// number: the state icon the ticket and landing lists already draw,
// carried as a sprite reference for the pages and as a key (data-ic) for
// the email renderer, which swaps it for a PNG because mail drops SVG.
type refIcon struct {
Key string // "ticket-closed", "landing-open", ...
Symbol string // the sprite symbol the lists use
Tone string // the list's colour class for that state
Label string // "ticket 305, closed"
}
func ticketIcon(tr TicketRef, number string) refIcon {
word := tr.State
if word == "" {
word = "open"
}
ic := refIcon{Key: "ticket-" + word, Symbol: "i-tkt-open", Tone: "g-open"}
switch word {
case "review":
ic.Tone = "g-review"
case "closed":
ic.Symbol, ic.Tone = "i-tkt-done", "g-done"
}
ic.Label = "ticket, " + word
if number != "" && !hexID.MatchString(number) {
ic.Label = "ticket " + number + ", " + word
}
return ic
}
// chatIcon is a message's: the chat bubble, in the link's own colour,
// since a message has no state to tint it by (#309).
func chatIcon(number string) refIcon {
return refIcon{Key: "chat", Symbol: "i-comment", Label: "message " + number}
}
func landingIcon(lr LandingRef) refIcon {
// The landing glyph, tinted by state, so a landing is never mistaken
// for a ticket (the circles) and each state reads without hover.
ic := refIcon{Key: "landing-" + lr.State, Symbol: "i-landing", Tone: "g-open"}
switch lr.State {
case "accepted":
ic.Tone = "g-done"
case "rejected":
ic.Tone = "g-rejected"
}
ic.Label = fmt.Sprintf("landing %d, %s", lr.Number, lr.State)
return ic
}
// wear puts the icon inside the anchor, ahead of its number, and names
// the reference for a reader who cannot see the glyph.
func (ic refIcon) wear(a *html.Node) {
svg := &html.Node{Type: html.ElementNode, Data: "svg", DataAtom: atom.Svg, Attr: []html.Attribute{
{Key: "class", Val: strings.TrimSpace("ic ref-ic " + ic.Tone)}, {Key: "aria-hidden", Val: "true"}}}
svg.AppendChild(&html.Node{Type: html.ElementNode, Data: "use", Attr: []html.Attribute{{Key: "href", Val: "#" + ic.Symbol}}})
a.InsertBefore(svg, a.FirstChild)
// The icon and its number are one unit and must not split across a
// line break; a link that carries the author's own words may wrap.
if len(anchorText(a)) <= 24 {
addClass(a, "ref-nw")
}
a.Attr = append(a.Attr,
html.Attribute{Key: "data-ic", Val: ic.Key},
html.Attribute{Key: "aria-label", Val: ic.Label},
html.Attribute{Key: "title", Val: ic.Label})
}
var refNumber = regexp.MustCompile(`(?i)^(?:ticket\s+)?(?:([a-zA-Z0-9][a-zA-Z0-9._-]*):)?[#!^]([1-9][0-9]*)$`)
// numberLabel is a typed reference reduced to what follows its sigil:
// the icon says what kind of thing it is, so "#305" and "!768" show
// 305 and 768 (a repo prefix stays: "demo:768"). Anything else the
// author wrote is left as it is.
func numberLabel(label string) string {
m := refNumber.FindStringSubmatch(strings.TrimSpace(label))
if m == nil {
return label
}
if m[1] != "" {
return m[1] + ":" + m[2]
}
return m[2]
}
func anchorText(n *html.Node) string {
var b strings.Builder
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.TextNode {
b.WriteString(c.Data)
}
}
return b.String()
}