@@ -1,6 +1,6 @@ {{/* Dot is an issues.Comment. */}} {{/* Dot is an changes.Comment. */}} {{define "comment"}} <div> <div> <div style="float: left; margin-right: 10px;">{{render (avatar .User)}}</div> <div id="comment-{{.ID}}" style="display: flex;" class="list-entry">
@@ -83,11 +83,11 @@ div.commit-message.list-entry-border { code { font-family: "Go Mono"; font-size: 12px; } /* Needed in issuesapp only because of something in parent containers... */ /* Needed in changesapp only because of something in parent containers... */ .markdown-body { word-break: break-word; } /* GFM style overrides. */
@@ -9,11 +9,11 @@ import ( "github.com/shurcooL/go/gopherjs_http" "github.com/shurcooL/httpfs/union" ) // Assets contains assets for issuesapp. // Assets contains assets for changesapp. var Assets = union.New(map[string]http.FileSystem{ "/script.js": gopherjs_http.Package("dmitri.shuralyov.com/changes/app/frontend"), "/assets": http.Dir(importPathToDir("dmitri.shuralyov.com/changes/app/_data")), })
@@ -1,4 +1,4 @@ //go:generate vfsgendev -source="dmitri.shuralyov.com/changes/app/assets".Assets // Package assets contains assets for issuesapp. // Package assets contains assets for changesapp. package assets
@@ -2,13 +2,13 @@ package changesapp import ( "strings" "dmitri.shuralyov.com/changes" "dmitri.shuralyov.com/changes/app/component" homecomponent "github.com/shurcooL/home/component" "github.com/shurcooL/htmlg" issuescomponent "github.com/shurcooL/issuesapp/component" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) type Commits struct { @@ -45,11 +45,11 @@ func (c Commit) Render() []*html.Node { avatarDiv := &html.Node{ Type: html.ElementNode, Data: atom.Div.String(), Attr: []html.Attribute{{Key: atom.Style.String(), Val: "margin-right: 6px;"}}, } htmlg.AppendChildren(avatarDiv, issuescomponent.Avatar{User: c.Author, Size: 32}.Render()...) htmlg.AppendChildren(avatarDiv, component.Avatar{User: c.Author, Size: 32}.Render()...) div.AppendChild(avatarDiv) titleAndByline := &html.Node{ Type: html.ElementNode, Data: atom.Div.String(), Attr: []html.Attribute{{Key: atom.Style.String(), Val: "flex-grow: 1;"}}, @@ -72,13 +72,13 @@ func (c Commit) Render() []*html.Node { } titleAndByline.AppendChild(title) byline := htmlg.DivClass("gray tiny") byline.Attr = append(byline.Attr, html.Attribute{Key: atom.Style.String(), Val: "margin-top: 2px;"}) htmlg.AppendChildren(byline, issuescomponent.User{User: c.Author}.Render()...) htmlg.AppendChildren(byline, component.User{User: c.Author}.Render()...) byline.AppendChild(htmlg.Text(" committed ")) htmlg.AppendChildren(byline, issuescomponent.Time{Time: c.AuthorTime}.Render()...) htmlg.AppendChildren(byline, component.Time{Time: c.AuthorTime}.Render()...) titleAndByline.AppendChild(byline) if commitBody != "" { pre := &html.Node{ Type: html.ElementNode, Data: atom.Pre.String(),
@@ -3,10 +3,11 @@ package component import ( "fmt" "dmitri.shuralyov.com/changes" "github.com/shurcooL/htmlg" issuescomponent "github.com/shurcooL/issuesapp/component" "github.com/shurcooL/octiconssvg" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) @@ -104,11 +105,11 @@ func (i ChangeEntry) Render() []*html.Node { for _, l := range i.Change.Labels { span := &html.Node{ Type: html.ElementNode, Data: atom.Span.String(), Attr: []html.Attribute{{Key: atom.Style.String(), Val: "margin-left: 4px;"}}, } htmlg.AppendChildren(span, Label{Label: l}.Render()...) htmlg.AppendChildren(span, issuescomponent.Label{Label: l}.Render()...) title.AppendChild(span) } titleAndByline.AppendChild(title) byline := htmlg.DivClass("gray tiny")
@@ -1,26 +1,25 @@ // Package component contains individual components that can render themselves as HTML. package component import ( "fmt" "image/color" "time" "dmitri.shuralyov.com/changes" "github.com/dustin/go-humanize" "github.com/shurcooL/htmlg" "github.com/shurcooL/issues" issuescomponent "github.com/shurcooL/issuesapp/component" "github.com/shurcooL/octiconssvg" "github.com/shurcooL/users" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) // Event is an event component. type Event struct { Event issues.Event Event changes.TimelineItem } func (e Event) Render() []*html.Node { // TODO: Make this much nicer. // <div class="list-entry event event-{{.Type}}"> @@ -29,18 +28,18 @@ func (e Event) Render() []*html.Node { // {{render (avatar .Actor)}} {{render (user .Actor)}} {{.Text}} {{render (time .CreatedAt)}} // </div> // </div> div := htmlg.DivClass("event-header") htmlg.AppendChildren(div, Avatar{User: e.Event.Actor, Size: 16, Inline: true}.Render()...) htmlg.AppendChildren(div, Avatar{User: e.Event.Actor, Size: 16, inline: true}.Render()...) htmlg.AppendChildren(div, User{e.Event.Actor}.Render()...) div.AppendChild(htmlg.Text(" ")) htmlg.AppendChildren(div, e.text()...) div.AppendChild(htmlg.Text(" ")) htmlg.AppendChildren(div, Time{e.Event.CreatedAt}.Render()...) outerDiv := htmlg.DivClass(fmt.Sprintf("list-entry event event-%s", e.Event.Type), outerDiv := htmlg.DivClass("list-entry event", e.icon(), div, ) return []*html.Node{outerDiv} } @@ -49,31 +48,31 @@ func (e Event) icon() *html.Node { var ( icon *html.Node color = "#767676" backgroundColor = "#f3f3f3" ) switch e.Event.Type { case issues.Reopened: icon = octiconssvg.PrimitiveDot() color, backgroundColor = "#fff", "#6cc644" case issues.Closed: switch e.Event.Payload.(type) { case changes.ClosedEvent: icon = octiconssvg.CircleSlash() color, backgroundColor = "#fff", "#bd2c00" case issues.Renamed: case changes.ReopenedEvent: icon = octiconssvg.PrimitiveDot() color, backgroundColor = "#fff", "#6cc644" case changes.RenamedEvent: icon = octiconssvg.Pencil() case issues.Labeled, issues.Unlabeled: case changes.LabeledEvent, changes.UnlabeledEvent: icon = octiconssvg.Tag() case issues.CommentDeleted: case changes.CommentDeletedEvent: icon = octiconssvg.X() case "ReviewRequestedEvent": case changes.ReviewRequestedEvent: icon = octiconssvg.Eye() case "ReviewRequestRemovedEvent": case changes.ReviewRequestRemovedEvent: icon = octiconssvg.X() case "MergedEvent": case changes.MergedEvent: icon = octiconssvg.GitMerge() color, backgroundColor = "#fff", "#6f42c1" case "ApprovedEvent": case changes.ApprovedEvent: icon = octiconssvg.Check() color, backgroundColor = "#fff", "#6cc644" default: icon = octiconssvg.PrimitiveDot() } @@ -86,50 +85,52 @@ func (e Event) icon() *html.Node { FirstChild: icon, } } func (e Event) text() []*html.Node { switch e.Event.Type { case issues.Reopened, issues.Closed: return []*html.Node{htmlg.Text(fmt.Sprintf("%s this", e.Event.Type))} case issues.Renamed: return []*html.Node{htmlg.Text("changed the title from "), htmlg.Strong(e.Event.Rename.From), htmlg.Text(" to "), htmlg.Strong(e.Event.Rename.To)} case issues.Labeled: switch p := e.Event.Payload.(type) { case changes.ClosedEvent: return []*html.Node{htmlg.Text("closed this")} case changes.ReopenedEvent: return []*html.Node{htmlg.Text("reopened this")} case changes.RenamedEvent: return []*html.Node{htmlg.Text("changed the title from "), htmlg.Strong(p.From), htmlg.Text(" to "), htmlg.Strong(p.To)} case changes.LabeledEvent: var ns []*html.Node ns = append(ns, htmlg.Text("added the ")) ns = append(ns, Label{Label: *e.Event.Label}.Render()...) ns = append(ns, issuescomponent.Label{Label: p.Label}.Render()...) ns = append(ns, htmlg.Text(" label")) return ns case issues.Unlabeled: case changes.UnlabeledEvent: var ns []*html.Node ns = append(ns, htmlg.Text("removed the ")) ns = append(ns, Label{Label: *e.Event.Label}.Render()...) ns = append(ns, issuescomponent.Label{Label: p.Label}.Render()...) ns = append(ns, htmlg.Text(" label")) return ns case issues.CommentDeleted: case changes.CommentDeletedEvent: return []*html.Node{htmlg.Text("deleted a comment")} case "ReviewRequestedEvent": case changes.ReviewRequestedEvent: ns := []*html.Node{htmlg.Text("requested a review from ")} ns = append(ns, Avatar{User: e.Event.RequestedReviewer, Size: 16, Inline: true}.Render()...) ns = append(ns, User{e.Event.RequestedReviewer}.Render()...) ns = append(ns, Avatar{User: p.RequestedReviewer, Size: 16, inline: true}.Render()...) ns = append(ns, User{p.RequestedReviewer}.Render()...) return ns case "ReviewRequestRemovedEvent": case changes.ReviewRequestRemovedEvent: ns := []*html.Node{htmlg.Text("removed the review request from ")} ns = append(ns, Avatar{User: e.Event.RequestedReviewer, Size: 16, Inline: true}.Render()...) ns = append(ns, User{e.Event.RequestedReviewer}.Render()...) ns = append(ns, Avatar{User: p.RequestedReviewer, Size: 16, inline: true}.Render()...) ns = append(ns, User{p.RequestedReviewer}.Render()...) return ns case "MergedEvent": case changes.MergedEvent: var ns []*html.Node ns = append(ns, htmlg.Text("merged commit ")) ns = append(ns, htmlg.Strong("d34db33f")) // TODO: e.MergedEvent.CommitID. ns = append(ns, htmlg.Strong(p.CommitID)) // TODO: Code{}, use CommitHTMLURL. ns = append(ns, htmlg.Text(" into ")) ns = append(ns, htmlg.Strong("master")) // TODO: e.MergedEvent.RefName. ns = append(ns, htmlg.Strong(p.RefName)) // TODO: Code{}. return ns case "ApprovedEvent": case changes.ApprovedEvent: return []*html.Node{htmlg.Text("approved these changes")} default: return []*html.Node{htmlg.Text(string(e.Event.Type))} return []*html.Node{htmlg.Text("unknown event")} // TODO: See if this is optimal. } } // ChangeStateBadge is a component that displays the state of a change // with a badge, who opened it, and when it was opened. @@ -237,48 +238,10 @@ color: ` + color + `;`, FirstChild: icon, } return []*html.Node{span} } // Label is a label component. type Label struct { Label issues.Label } func (l Label) Render() []*html.Node { // TODO: Make this much nicer. // <span style="...; color: {{.fontColor}}; background-color: {{.Color.HexString}};">{{.Name}}</span> span := &html.Node{ Type: html.ElementNode, Data: atom.Span.String(), Attr: []html.Attribute{{ Key: atom.Style.String(), Val: `display: inline-block; font-size: 12px; line-height: 1.2; padding: 0px 3px 0px 3px; border-radius: 2px; color: ` + l.fontColor() + `; background-color: ` + l.Label.Color.HexString() + `;`, }}, } span.AppendChild(htmlg.Text(l.Label.Name)) return []*html.Node{span} } // fontColor returns one of "#fff" or "#000", whichever is a better fit for // the font color given the label color. func (l Label) fontColor() string { // Convert label color to 8-bit grayscale, and make a decision based on that. switch y := color.GrayModel.Convert(l.Label.Color).(color.Gray).Y; { case y < 128: return "#fff" case y >= 128: return "#000" } panic("unreachable") } // User is a user component. type User struct { User users.User } @@ -297,21 +260,21 @@ func (u User) Render() []*html.Node { } // Avatar is an avatar component. type Avatar struct { User users.User Size int // In pixels, e.g., 48. Inline bool Size int // In pixels, e.g., 48. inline bool // inline is experimental; so keep it contained to this package only for now. } func (a Avatar) Render() []*html.Node { // TODO: Make this much nicer. // <a style="..." href="{{.User.HTMLURL}}" tabindex=-1> // <img style="..." width="{{.Size}}" height="{{.Size}}" src="{{.User.AvatarURL}}"> // </a> imgStyle := "border-radius: 3px;" if a.Inline { if a.inline { imgStyle += " vertical-align: middle; margin-right: 4px;" } return []*html.Node{{ Type: html.ElementNode, Data: atom.A.String(), Attr: []html.Attribute{
@@ -1,47 +1,59 @@ package changesapp import ( "bytes" "fmt" "html/template" "sort" "strings" "time" "github.com/shurcooL/issues" "dmitri.shuralyov.com/changes" "dmitri.shuralyov.com/changes/app/component" "github.com/shurcooL/highlight_diff" "github.com/shurcooL/htmlg" "github.com/shurcooL/users" "github.com/sourcegraph/annotate" "golang.org/x/net/html" "golang.org/x/net/html/atom" "sourcegraph.com/sourcegraph/go-diff/diff" ) // timelineItem represents a timeline item for display purposes. type timelineItem struct { // TimelineItem can be one of issues.Comment, issues.Event. // TimelineItem can be one of changes.Comment, changes.TimelineItem. TimelineItem interface{} } func (i timelineItem) TemplateName() string { switch i.TimelineItem.(type) { case issues.Comment: case changes.Comment: return "comment" case issues.Event: case changes.TimelineItem: return "event" default: panic(fmt.Errorf("unknown item type %T", i.TimelineItem)) } } func (i timelineItem) CreatedAt() time.Time { switch i := i.TimelineItem.(type) { case issues.Comment: case changes.Comment: return i.CreatedAt case issues.Event: case changes.TimelineItem: return i.CreatedAt default: panic(fmt.Errorf("unknown item type %T", i)) } } func (i timelineItem) ID() uint64 { switch i := i.TimelineItem.(type) { case issues.Comment: case changes.Comment: return i.ID case issues.Event: case changes.TimelineItem: return i.ID default: panic(fmt.Errorf("unknown item type %T", i)) } } @@ -56,5 +68,214 @@ func (s byCreatedAtID) Less(i, j int) bool { return s[i].ID() < s[j].ID() } return s[i].CreatedAt().Before(s[j].CreatedAt()) } func (s byCreatedAtID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // TODO: Dedup. // tabnav is a left-aligned horizontal row of tabs Primer CSS component. // // http://primercss.io/nav/#tabnav type tabnav struct { Tabs []tab } func (t tabnav) Render() []*html.Node { nav := &html.Node{ Type: html.ElementNode, Data: atom.Nav.String(), Attr: []html.Attribute{{Key: atom.Class.String(), Val: "tabnav-tabs"}}, } for _, t := range t.Tabs { htmlg.AppendChildren(nav, t.Render()...) } return []*html.Node{htmlg.DivClass("tabnav", nav)} } // tab is a single tab entry within a tabnav. type tab struct { Content htmlg.Component URL string Selected bool } func (t tab) Render() []*html.Node { aClass := "tabnav-tab" if t.Selected { aClass += " selected" } a := &html.Node{ Type: html.ElementNode, Data: atom.A.String(), Attr: []html.Attribute{ {Key: atom.Href.String(), Val: t.URL}, {Key: atom.Class.String(), Val: aClass}, }, } htmlg.AppendChildren(a, t.Content.Render()...) return []*html.Node{a} } type contentCounter struct { Content htmlg.Component Count int } func (cc contentCounter) Render() []*html.Node { var ns []*html.Node ns = append(ns, cc.Content.Render()...) ns = append(ns, htmlg.SpanClass("counter", htmlg.Text(fmt.Sprint(cc.Count)))) return ns } // iconText is an icon with text on the right. // Icon must be not nil. type iconText struct { Icon func() *html.Node // Must be not nil. Text string } func (it iconText) Render() []*html.Node { icon := htmlg.Span(it.Icon()) icon.Attr = append(icon.Attr, html.Attribute{ Key: atom.Style.String(), Val: "margin-right: 4px;", }) text := htmlg.Text(it.Text) return []*html.Node{icon, text} } // commitMessage ... type commitMessage struct { CommitHash string Subject string Body string Author users.User AuthorTime time.Time PrevSHA, NextSHA string // Empty if none. } func (c commitMessage) Avatar() template.HTML { return template.HTML(htmlg.RenderComponentsString(component.Avatar{User: c.Author, Size: 24})) } func (c commitMessage) User() template.HTML { return template.HTML(htmlg.RenderComponentsString(component.User{User: c.Author})) } func (c commitMessage) Time() template.HTML { return template.HTML(htmlg.RenderComponentsString(component.Time{Time: c.AuthorTime})) } // fileDiff represents a file diff for display purposes. type fileDiff struct { *diff.FileDiff } func (f fileDiff) Title() (template.HTML, error) { old := strings.TrimPrefix(f.OrigName, "a/") new := strings.TrimPrefix(f.NewName, "b/") switch { case old != "/dev/null" && new != "/dev/null" && old == new: // Modified. return template.HTML(html.EscapeString(new)), nil case old != "/dev/null" && new != "/dev/null" && old != new: // Renamed. return template.HTML(html.EscapeString(old + " -> " + new)), nil case old == "/dev/null" && new != "/dev/null": // Added. return template.HTML(html.EscapeString(new)), nil case old != "/dev/null" && new == "/dev/null": // Removed. return template.HTML("<strikethrough>" + html.EscapeString(old) + "</strikethrough>"), nil default: return "", fmt.Errorf("unexpected *diff.FileDiff: %+v", f) } } func (f fileDiff) Diff() (template.HTML, error) { hunks, err := diff.PrintHunks(f.Hunks) if err != nil { return "", err } diff, err := highlightDiff(hunks) if err != nil { return "", err } return template.HTML(diff), nil } // highlightDiff highlights the src diff, returning the annotated HTML. func highlightDiff(src []byte) ([]byte, error) { anns, err := highlight_diff.Annotate(src) if err != nil { return nil, err } lines := bytes.Split(src, []byte("\n")) lineStarts := make([]int, len(lines)) var offset int for lineIndex := 0; lineIndex < len(lines); lineIndex++ { lineStarts[lineIndex] = offset offset += len(lines[lineIndex]) + 1 } lastDel, lastIns := -1, -1 for lineIndex := 0; lineIndex < len(lines); lineIndex++ { var lineFirstChar byte if len(lines[lineIndex]) > 0 { lineFirstChar = lines[lineIndex][0] } switch lineFirstChar { case '+': if lastIns == -1 { lastIns = lineIndex } case '-': if lastDel == -1 { lastDel = lineIndex } default: if lastDel != -1 || lastIns != -1 { if lastDel == -1 { lastDel = lastIns } else if lastIns == -1 { lastIns = lineIndex } beginOffsetLeft := lineStarts[lastDel] endOffsetLeft := lineStarts[lastIns] beginOffsetRight := lineStarts[lastIns] endOffsetRight := lineStarts[lineIndex] anns = append(anns, &annotate.Annotation{Start: beginOffsetLeft, End: endOffsetLeft, Left: []byte(`<span class="gd input-block">`), Right: []byte(`</span>`), WantInner: 0}) anns = append(anns, &annotate.Annotation{Start: beginOffsetRight, End: endOffsetRight, Left: []byte(`<span class="gi input-block">`), Right: []byte(`</span>`), WantInner: 0}) if '@' != lineFirstChar { //leftContent := string(src[beginOffsetLeft:endOffsetLeft]) //rightContent := string(src[beginOffsetRight:endOffsetRight]) // This is needed to filter out the "-" and "+" at the beginning of each line from being highlighted. // TODO: Still not completely filtered out. leftContent := "" for line := lastDel; line < lastIns; line++ { leftContent += "\x00" + string(lines[line][1:]) + "\n" } rightContent := "" for line := lastIns; line < lineIndex; line++ { rightContent += "\x00" + string(lines[line][1:]) + "\n" } var sectionSegments [2][]*annotate.Annotation highlight_diff.HighlightedDiffFunc(leftContent, rightContent, §ionSegments, [2]int{beginOffsetLeft, beginOffsetRight}) anns = append(anns, sectionSegments[0]...) anns = append(anns, sectionSegments[1]...) } } lastDel, lastIns = -1, -1 } } sort.Sort(anns) out, err := annotate.Annotate(src, anns, template.HTMLEscape) if err != nil { return nil, err } return out, nil }
@@ -1,6 +1,6 @@ // frontend script for issuesapp. // frontend script for changesapp. // // It's a Go package meant to be compiled with GOARCH=js // and executed in a browser, where the DOM is available. package main
@@ -25,11 +25,10 @@ import ( "github.com/shurcooL/github_flavored_markdown" "github.com/shurcooL/htmlg" "github.com/shurcooL/httperror" "github.com/shurcooL/httpfs/html/vfstemplate" "github.com/shurcooL/httpgzip" "github.com/shurcooL/issues" "github.com/shurcooL/notifications" "github.com/shurcooL/octiconssvg" "github.com/shurcooL/reactions" reactionscomponent "github.com/shurcooL/reactions/component" "github.com/shurcooL/users" @@ -571,11 +570,11 @@ func loadTemplates(state common.State, bodyPre string) (*template.Template, erro }, "render": func(c htmlg.Component) template.HTML { return template.HTML(htmlg.Render(c.Render()...)) }, "event": func(e issues.Event) htmlg.Component { return component.Event{Event: e} }, "event": func(e changes.TimelineItem) htmlg.Component { return component.Event{Event: e} }, "changeStateBadge": func(c changes.Change) htmlg.Component { return component.ChangeStateBadge{Change: c} }, "time": func(t time.Time) htmlg.Component { return component.Time{Time: t} }, "user": func(u users.User) htmlg.Component { return component.User{User: u} }, "avatar": func(u users.User) htmlg.Component { return component.Avatar{User: u, Size: 48} }, })
@@ -1,228 +0,0 @@ package changesapp import ( "bytes" "fmt" "html/template" "sort" "strings" "time" "github.com/shurcooL/highlight_diff" "github.com/shurcooL/htmlg" issuescomponent "github.com/shurcooL/issuesapp/component" "github.com/shurcooL/users" "github.com/sourcegraph/annotate" "golang.org/x/net/html" "golang.org/x/net/html/atom" "sourcegraph.com/sourcegraph/go-diff/diff" ) // TODO: Dedup. // tabnav is a left-aligned horizontal row of tabs Primer CSS component. // // http://primercss.io/nav/#tabnav type tabnav struct { Tabs []tab } func (t tabnav) Render() []*html.Node { nav := &html.Node{ Type: html.ElementNode, Data: atom.Nav.String(), Attr: []html.Attribute{{Key: atom.Class.String(), Val: "tabnav-tabs"}}, } for _, t := range t.Tabs { htmlg.AppendChildren(nav, t.Render()...) } return []*html.Node{htmlg.DivClass("tabnav", nav)} } // tab is a single tab entry within a tabnav. type tab struct { Content htmlg.Component URL string Selected bool } func (t tab) Render() []*html.Node { aClass := "tabnav-tab" if t.Selected { aClass += " selected" } a := &html.Node{ Type: html.ElementNode, Data: atom.A.String(), Attr: []html.Attribute{ {Key: atom.Href.String(), Val: t.URL}, {Key: atom.Class.String(), Val: aClass}, }, } htmlg.AppendChildren(a, t.Content.Render()...) return []*html.Node{a} } type contentCounter struct { Content htmlg.Component Count int } func (cc contentCounter) Render() []*html.Node { var ns []*html.Node ns = append(ns, cc.Content.Render()...) ns = append(ns, htmlg.SpanClass("counter", htmlg.Text(fmt.Sprint(cc.Count)))) return ns } // iconText is an icon with text on the right. // Icon must be not nil. type iconText struct { Icon func() *html.Node // Must be not nil. Text string } func (it iconText) Render() []*html.Node { icon := htmlg.Span(it.Icon()) icon.Attr = append(icon.Attr, html.Attribute{ Key: atom.Style.String(), Val: "margin-right: 4px;", }) text := htmlg.Text(it.Text) return []*html.Node{icon, text} } // commitMessage ... type commitMessage struct { CommitHash string Subject string Body string Author users.User AuthorTime time.Time PrevSHA, NextSHA string // Empty if none. } func (c commitMessage) Avatar() template.HTML { return template.HTML(htmlg.RenderComponentsString(issuescomponent.Avatar{User: c.Author, Size: 24})) } func (c commitMessage) User() template.HTML { return template.HTML(htmlg.RenderComponentsString(issuescomponent.User{User: c.Author})) } func (c commitMessage) Time() template.HTML { return template.HTML(htmlg.RenderComponentsString(issuescomponent.Time{Time: c.AuthorTime})) } // fileDiff represents a file diff for display purposes. type fileDiff struct { *diff.FileDiff } func (f fileDiff) Title() (template.HTML, error) { old := strings.TrimPrefix(f.OrigName, "a/") new := strings.TrimPrefix(f.NewName, "b/") switch { case old != "/dev/null" && new != "/dev/null" && old == new: // Modified. return template.HTML(html.EscapeString(new)), nil case old != "/dev/null" && new != "/dev/null" && old != new: // Renamed. return template.HTML(html.EscapeString(old + " -> " + new)), nil case old == "/dev/null" && new != "/dev/null": // Added. return template.HTML(html.EscapeString(new)), nil case old != "/dev/null" && new == "/dev/null": // Removed. return template.HTML("<strikethrough>" + html.EscapeString(old) + "</strikethrough>"), nil default: return "", fmt.Errorf("unexpected *diff.FileDiff: %+v", f) } } func (f fileDiff) Diff() (template.HTML, error) { hunks, err := diff.PrintHunks(f.Hunks) if err != nil { return "", err } diff, err := highlightDiff(hunks) if err != nil { return "", err } return template.HTML(diff), nil } // highlightDiff highlights the src diff, returning the annotated HTML. func highlightDiff(src []byte) ([]byte, error) { anns, err := highlight_diff.Annotate(src) if err != nil { return nil, err } lines := bytes.Split(src, []byte("\n")) lineStarts := make([]int, len(lines)) var offset int for lineIndex := 0; lineIndex < len(lines); lineIndex++ { lineStarts[lineIndex] = offset offset += len(lines[lineIndex]) + 1 } lastDel, lastIns := -1, -1 for lineIndex := 0; lineIndex < len(lines); lineIndex++ { var lineFirstChar byte if len(lines[lineIndex]) > 0 { lineFirstChar = lines[lineIndex][0] } switch lineFirstChar { case '+': if lastIns == -1 { lastIns = lineIndex } case '-': if lastDel == -1 { lastDel = lineIndex } default: if lastDel != -1 || lastIns != -1 { if lastDel == -1 { lastDel = lastIns } else if lastIns == -1 { lastIns = lineIndex } beginOffsetLeft := lineStarts[lastDel] endOffsetLeft := lineStarts[lastIns] beginOffsetRight := lineStarts[lastIns] endOffsetRight := lineStarts[lineIndex] anns = append(anns, &annotate.Annotation{Start: beginOffsetLeft, End: endOffsetLeft, Left: []byte(`<span class="gd input-block">`), Right: []byte(`</span>`), WantInner: 0}) anns = append(anns, &annotate.Annotation{Start: beginOffsetRight, End: endOffsetRight, Left: []byte(`<span class="gi input-block">`), Right: []byte(`</span>`), WantInner: 0}) if '@' != lineFirstChar { //leftContent := string(src[beginOffsetLeft:endOffsetLeft]) //rightContent := string(src[beginOffsetRight:endOffsetRight]) // This is needed to filter out the "-" and "+" at the beginning of each line from being highlighted. // TODO: Still not completely filtered out. leftContent := "" for line := lastDel; line < lastIns; line++ { leftContent += "\x00" + string(lines[line][1:]) + "\n" } rightContent := "" for line := lastIns; line < lineIndex; line++ { rightContent += "\x00" + string(lines[line][1:]) + "\n" } var sectionSegments [2][]*annotate.Annotation highlight_diff.HighlightedDiffFunc(leftContent, rightContent, §ionSegments, [2]int{beginOffsetLeft, beginOffsetRight}) anns = append(anns, sectionSegments[0]...) anns = append(anns, sectionSegments[1]...) } } lastDel, lastIns = -1, -1 } } sort.Sort(anns) out, err := annotate.Annotate(src, anns, template.HTMLEscape) if err != nil { return nil, err } return out, nil }