dmitri.shuralyov.com/html/belt/...

Add Issue, Change, Commit components.

Follows https://dmitri.shuralyov.com/state/...$commit/28bcc343414c6adcd7b6911f9d0ef1ad6fbf30ae.
dmitshur committed 6 years ago commit f6fb90ff73e49d9904a6a8a5561e437734a2ea57
Collapse all
belt.go
@@ -1,34 +1,184 @@
// Package belt is an opinionated collection of HTML components
// for shared use by multiple web apps.
package belt

import (
	"fmt"
	"strings"

	"dmitri.shuralyov.com/state"
	"github.com/shurcooL/htmlg"
	"github.com/shurcooL/octiconssvg"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
)

// Reference is a component that displays a reference (branch or tag). E.g., "master".
type Reference struct {
	Name          string
	Strikethrough bool
// Issue is a component that displays an issue, with a state icon and title.
type Issue struct {
	State   state.Issue
	Title   string
	HTMLURL string
	Short   bool
}

func (r Reference) Render() []*html.Node {
	codeStyle := `padding: 2px 6px;
background-color: rgb(232, 241, 246);
border-radius: 3px;`
	if r.Strikethrough {
		codeStyle += `text-decoration: line-through; color: gray;`
func (i Issue) Render() []*html.Node {
	n := iconLink{
		Text:    i.Title,
		Tooltip: i.Title,
		URL:     i.HTMLURL,
	}
	code := &html.Node{
		Type: html.ElementNode, Data: atom.Code.String(),
		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: codeStyle}},
		FirstChild: htmlg.Text(r.Name),
	if i.Short {
		n.Text = shortTitle(i.Title)
	}
	return []*html.Node{code}
	switch i.State {
	case state.IssueOpen:
		n.Icon = octiconssvg.IssueOpened
		n.IconColor = &rgb{R: 0x6c, G: 0xc6, B: 0x44} // Green.
	case state.IssueClosed:
		n.Icon = octiconssvg.IssueClosed
		n.IconColor = &rgb{R: 0xbd, G: 0x2c, B: 0x00} // Red.
	}
	return n.Render()
}

// Change is a component that displays a change, with a state icon and title.
type Change struct {
	State   state.Change
	Title   string
	HTMLURL string
	Short   bool
}

func (c Change) Render() []*html.Node {
	n := iconLink{
		Text:    c.Title,
		Tooltip: c.Title,
		URL:     c.HTMLURL,
	}
	if c.Short {
		n.Text = shortTitle(c.Title)
	}
	switch c.State {
	case state.ChangeOpen:
		n.Icon = octiconssvg.GitPullRequest
		n.IconColor = &rgb{R: 0x6c, G: 0xc6, B: 0x44} // Green.
	case state.ChangeClosed:
		n.Icon = octiconssvg.GitPullRequest
		n.IconColor = &rgb{R: 0xbd, G: 0x2c, B: 0x00} // Red.
	case state.ChangeMerged:
		n.Icon = octiconssvg.GitMerge
		n.IconColor = &rgb{R: 0x6e, G: 0x54, B: 0x94} // Purple.
	}
	return n.Render()
}

func shortTitle(s string) string {
	if len(s) <= 36 {
		return s
	}
	return s[:35] + "…"
}

// iconLink consists of an icon and a text link.
// Icon must be not nil.
type iconLink struct {
	Text      string
	Tooltip   string
	URL       string
	Black     bool              // Black link.
	Icon      func() *html.Node // Not nil.
	IconColor *rgb              // Optional icon color override.
}

func (d iconLink) Render() []*html.Node {
	a := &html.Node{
		Type: html.ElementNode, Data: atom.A.String(),
		Attr: []html.Attribute{{Key: atom.Href.String(), Val: d.URL}},
	}
	if d.Tooltip != "" {
		a.Attr = append(a.Attr, html.Attribute{Key: atom.Title.String(), Val: d.Tooltip})
	}
	if d.Black {
		a.Attr = append(a.Attr, html.Attribute{Key: atom.Class.String(), Val: "black"})
	}
	iconSpanStyle := "margin-right: 4px;"
	if d.IconColor != nil {
		iconSpanStyle += fmt.Sprintf(" color: %s;", d.IconColor.HexString())
	}
	a.AppendChild(&html.Node{
		Type: html.ElementNode, Data: atom.Span.String(),
		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: iconSpanStyle}},
		FirstChild: d.Icon(),
	})
	a.AppendChild(htmlg.Text(d.Text))
	return []*html.Node{a}
}

// rgb represents a 24-bit color without alpha channel.
type rgb struct {
	R, G, B uint8
}

// HexString returns a hexadecimal color string. For example, "#ff0000" for red.
func (c rgb) HexString() string {
	return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}

// Commit is a component that displays a commit, with an author and title.
type Commit struct {
	SHA             string
	Message         string
	AuthorAvatarURL string
	HTMLURL         string // Optional.
	Short           bool
}

func (c Commit) Render() []*html.Node {
	avatar := &html.Node{
		Type: html.ElementNode, Data: atom.Img.String(),
		Attr: []html.Attribute{
			{Key: atom.Src.String(), Val: c.AuthorAvatarURL},
			{Key: atom.Style.String(), Val: "width: 16px; height: 16px; vertical-align: top; margin-right: 4px;"},
		},
	}
	commitID := CommitID{SHA: c.SHA, HTMLURL: c.HTMLURL}
	message := &html.Node{
		Type: html.ElementNode, Data: atom.Span.String(),
		Attr: []html.Attribute{
			{Key: atom.Style.String(), Val: "margin-left: 4px;"},
			{Key: atom.Title.String(), Val: c.Message},
		},
	}
	switch c.Short {
	case false:
		message.AppendChild(htmlg.Text(firstParagraph(c.Message)))
	case true:
		message.AppendChild(htmlg.Text(shortCommit(firstParagraph(c.Message))))
	}

	var ns []*html.Node
	ns = append(ns, avatar)
	ns = append(ns, commitID.Render()...)
	ns = append(ns, message)
	return ns
}

func shortCommit(s string) string {
	if len(s) <= 24 {
		return s
	}
	return s[:23] + "…"
}

// firstParagraph returns the first paragraph of text s.
func firstParagraph(s string) string {
	i := strings.Index(s, "\n\n")
	if i == -1 {
		return s
	}
	return s[:i]
}

// CommitID is a component that displays a commit ID. E.g., "c0de1234".
type CommitID struct {
	SHA     string
@@ -53,5 +203,26 @@ func (c CommitID) Render() []*html.Node {
			FirstChild: sha,
		}
	}
	return []*html.Node{sha}
}

// Reference is a component that displays a reference (branch or tag). E.g., "master".
type Reference struct {
	Name          string
	Strikethrough bool
}

func (r Reference) Render() []*html.Node {
	codeStyle := `padding: 2px 6px;
background-color: rgb(232, 241, 246);
border-radius: 3px;`
	if r.Strikethrough {
		codeStyle += `text-decoration: line-through; color: gray;`
	}
	code := &html.Node{
		Type: html.ElementNode, Data: atom.Code.String(),
		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: codeStyle}},
		FirstChild: htmlg.Text(r.Name),
	}
	return []*html.Node{code}
}