Skip to content
quarry Artifact [0ed28e279b]
Login

Artifact [0ed28e279b]

Artifact 0ed28e279b43852b0574be9b424d3868709d93e2ed6259911db577293df05b43:


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()
}