Fancy opengraph

release
Carsten Kragelund 2023-05-03 23:36:43 +02:00 committed by Carsten Kragelund Jørgensen
parent 8ceb78caad
commit a990c55476
10 changed files with 1606 additions and 0 deletions

@ -119,6 +119,10 @@ require (
xorm.io/xorm v1.3.3-0.20221209153726-f1bfc5ce9830
)
require github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
require golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
require (
cloud.google.com/go/compute v1.7.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
@ -203,7 +207,9 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jbuchbinder/gg v1.3.1-0.20220522202534-b71f553fdd3d
github.com/jessevdk/go-flags v1.5.0 // indirect
github.com/jiro4989/textimg/v3 v3.1.8
github.com/josharian/intern v1.0.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect

@ -535,6 +535,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -787,10 +789,16 @@ github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jbuchbinder/gg v1.3.0 h1:nfHEGrrXMCMIlLQIooBwPwn2IqEWhLQQ1s0WPmBpwdw=
github.com/jbuchbinder/gg v1.3.0/go.mod h1:V0Eu/AInMEKfU25ID6D/0FRIGj+Nz1EXId1Igjq1XI4=
github.com/jbuchbinder/gg v1.3.1-0.20220522202534-b71f553fdd3d h1:xUaIzSyN5X8wtiiOfxsLG+uFswf7CdSgd3f62t99XhE=
github.com/jbuchbinder/gg v1.3.1-0.20220522202534-b71f553fdd3d/go.mod h1:Rjxgiu1UOnrvAD1gplSXdFBNpytrSarkIqp98olsi/g=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jhillyerd/enmime v0.10.1 h1:3VP8gFhK7R948YJBrna5bOgnTXEuPAoICo79kKkBKfA=
github.com/jhillyerd/enmime v0.10.1/go.mod h1:Qpe8EEemJMFAF8+NZoWdpXvK2Yb9dRF0k/z6mkcDHsA=
github.com/jiro4989/textimg/v3 v3.1.8 h1:tfzkegBW59IRkpn/BLMUPTK0XRXW61xQQ4sYTzMCZDI=
github.com/jiro4989/textimg/v3 v3.1.8/go.mod h1:ohDSZdKqvQXFG9rdAAf/AVNxyvtjh32kj0nbW/zXo3k=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@ -1382,6 +1390,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

@ -0,0 +1,73 @@
package image
import (
"bytes"
"embed"
"image/color"
"strings"
"github.com/jbuchbinder/gg"
"github.com/jiro4989/textimg/v3/token"
)
//go:embed fonts
var fonts embed.FS
func Draw(tokens token.Tokens) ([]byte, error) {
foreground := color.RGBA{205, 214, 244, 255}
background := color.RGBA{30, 30, 46, 255}
dc := gg.NewContext(1200, 630)
fgCol := foreground
bgCol := background
dc.SetColor(bgCol)
if err := dc.LoadFontFaceFS(fonts, "fonts/JetBrainsMono-Regular.ttf", 20); err != nil {
return nil, err
}
dc.Clear()
curX, curY := 50.0, 50.0
for _, t := range tokens {
switch t.Kind {
case token.KindColor:
switch t.ColorType {
case token.ColorTypeReset:
fgCol = foreground
bgCol = background
case token.ColorTypeResetForeground:
fgCol = foreground
case token.ColorTypeResetBackground:
bgCol = background
case token.ColorTypeReverse:
fgCol, bgCol = bgCol, fgCol
case token.ColorTypeForeground:
fgCol = color.RGBA(t.Color)
case token.ColorTypeBackground:
bgCol = color.RGBA(t.Color)
}
case token.KindText:
w, h := dc.MeasureMultilineString(t.Text, 1.0)
dc.Push()
dc.SetColor(bgCol)
dc.DrawRectangle(curX, curY, w, h)
dc.Fill()
dc.Pop()
dc.SetColor(fgCol)
dc.DrawStringAnchored(strings.ReplaceAll(strings.ReplaceAll(t.Text, "\t", " "), "\n", ""), curX, curY, 0.0, 1.0)
curX += w
if strings.Contains(t.Text, "\n") {
curY += h
curX = 50
}
}
}
dc.Push()
dc.SetColor(background)
dc.DrawRectangle(1150, 0, 50, 630)
dc.DrawRectangle(0, 580, 1200, 50)
dc.Fill()
dc.Pop()
buffer := new(bytes.Buffer)
dc.EncodePNG(buffer)
return buffer.Bytes(), nil
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,74 @@
package parser
import (
"strconv"
"github.com/jiro4989/textimg/v3/color"
"github.com/jiro4989/textimg/v3/token"
)
type ParserFunc struct {
// pegが生成するTokensと名前が衝突するので別名にする
Tk token.Tokens
}
func Parse(s string) (token.Tokens, error) {
p := &Parser{Buffer: s}
if err := p.Init(); err != nil {
return nil, err
}
if err := p.Parse(); err != nil {
return nil, err
}
p.Execute()
return p.Tk, nil
}
func (p *ParserFunc) pushResetColor() {
p.Tk = append(p.Tk, token.NewResetColor())
}
func (p *ParserFunc) pushResetForegroundColor() {
p.Tk = append(p.Tk, token.NewResetForegroundColor())
}
func (p *ParserFunc) pushResetBackgroundColor() {
p.Tk = append(p.Tk, token.NewResetBackgroundColor())
}
func (p *ParserFunc) pushReverseColor() {
p.Tk = append(p.Tk, token.NewReverseColor())
}
func (p *ParserFunc) pushText(text string) {
p.Tk = append(p.Tk, token.NewText(text))
}
func (p *ParserFunc) pushStandardColorWithCategory(text string) {
p.Tk = append(p.Tk, token.NewStandardColorWithCategory(text))
}
func (p *ParserFunc) pushExtendedColor(text string) {
p.Tk = append(p.Tk, token.NewExtendedColor(text))
}
func (p *ParserFunc) setExtendedColor256(text string) {
n, _ := strconv.ParseUint(text, 10, 8)
p.Tk[len(p.Tk)-1].Color = color.Map256[int(n)]
}
func (p *ParserFunc) setExtendedColorR(text string) {
n, _ := strconv.ParseUint(text, 10, 8)
p.Tk[len(p.Tk)-1].Color.R = uint8(n)
}
func (p *ParserFunc) setExtendedColorG(text string) {
n, _ := strconv.ParseUint(text, 10, 8)
p.Tk[len(p.Tk)-1].Color.G = uint8(n)
}
func (p *ParserFunc) setExtendedColorB(text string) {
n, _ := strconv.ParseUint(text, 10, 8)
p.Tk[len(p.Tk)-1].Color.B = uint8(n)
}

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/util"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
@ -195,6 +196,65 @@ func File(fileName, language string, code []byte) ([]string, string, error) {
return lines, lexerName, nil
}
func AnsiFile(fileName, language string, code []byte) ([]string, string, error) {
NewContext()
if len(code) > sizeLimit {
code = code[:sizeLimit]
}
var lexer chroma.Lexer
// provided language overrides everything
if language != "" {
lexer = lexers.Get(language)
}
if lexer == nil {
if val, ok := highlightMapping[filepath.Ext(fileName)]; ok {
lexer = lexers.Get(val)
}
}
if lexer == nil {
guessLanguage := analyze.GetCodeLanguage(fileName, code)
lexer = lexers.Get(guessLanguage)
if lexer == nil {
lexer = lexers.Match(fileName)
if lexer == nil {
lexer = lexers.Fallback
}
}
}
lexerName := formatLexerName(lexer.Config().Name)
iterator, err := lexer.Tokenise(nil, string(code))
if err != nil {
return nil, "", fmt.Errorf("can't tokenize code: %w", err)
}
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
ansiBuf := &bytes.Buffer{}
formatter := formatters.TTY16m
lines := make([]string, 0, len(tokensLines))
for _, tokens := range tokensLines {
iterator = chroma.Literator(tokens...)
err = formatter.Format(ansiBuf, styles.CatppuccinMocha, iterator)
if err != nil {
return nil, "", fmt.Errorf("can't format code: %w", err)
}
lines = append(lines, strings.Replace(ansiBuf.String(), "\033[3m", "", -1))
ansiBuf.Reset()
}
return lines, lexerName, nil
}
// PlainText returns non-highlighted HTML for code
func PlainText(code []byte) []string {
r := bufio.NewReader(bytes.NewReader(code))

@ -26,6 +26,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/codeimage/image"
"code.gitea.io/gitea/modules/codeimage/parser"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
@ -509,6 +511,12 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
status = status.Or(statuses[i])
}
ogImg := ctx.Repo.Repository.HTMLURL() + "/src/og/" + ctx.Repo.BranchName
if len(ctx.Repo.TreePath) > 0 {
ogImg += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
ctx.Data["OgImage"] = ogImg
ctx.Data["EscapeStatus"] = status
ctx.Data["FileContent"] = fileContent
ctx.Data["LineEscapeStatus"] = statuses
@ -699,6 +707,55 @@ func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
}
}
func OgImage(ctx *context.Context) {
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
log.Error("%v", err)
return
}
blob := entry.Blob()
buf, dataRc, _, err := getFileReader(ctx.Repo.Repository.ID, blob)
if err != nil {
ctx.ServerError("getFileReader", err)
return
}
defer dataRc.Close()
language := ""
indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
if err == nil {
defer deleteTemporaryFile()
filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
CachedOnly: true,
Attributes: []string{"linguist-language", "gitlab-language"},
Filenames: []string{ctx.Repo.TreePath},
IndexFile: indexFilename,
WorkTree: worktree,
})
if err != nil {
log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
if language == "" || language == "unspecified" {
language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
}
if language == "unspecified" {
language = ""
}
}
ansiContent, _, _ := highlight.AnsiFile(blob.Name(), language, buf)
log.Info("%s", ansiContent[0])
tks, _ := parser.Parse(strings.Join(ansiContent, ""))
img, err := image.Draw(tks)
if err != nil {
log.Error("Drawing image failed: %v", err)
}
ctx.Write(img)
}
// Home render repository home page
func Home(ctx *context.Context) {
if setting.EnableFeed {
@ -965,6 +1022,11 @@ func renderCode(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRepoHome)
}
func HighlightCodeToImage() error {
return nil
}
// RenderUserCards render a page show users according the input template
func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) {
page := ctx.FormInt("page")

@ -1470,6 +1470,7 @@ func RegisterRoutes(m *web.Route) {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home)
m.Get("/og/*", context.RepoRefByType(context.RepoRefBranch), repo.OgImage)
// "/*" route is deprecated, and kept for backward compatibility
m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home)
}, repo.SetEditorconfigIfExists)

@ -37,6 +37,19 @@
{{if .Owner.Description}}
<meta property="og:description" content="{{.Owner.Description}}">
{{end}}
{{else if .PageIsViewCode }}
<meta name="twitter:image:src" content="{{.OgImage}}">
<meta name="twitter:site" content="@gitea">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{.Repository.Name}}/{{.FileName}}">
<meta name="twitter:description" content="{{.Repository.Description}}">
<meta property="og:image" content="{{.OgImage}}">
<meta property="og:image:alt" content="{{.Repository.Description}}">
<meta property="og:site_name" content="{{AppName}}">
<meta property="og:type" content="object">
<meta property="og:title" content="{{.Repository.Name}}/{{.FileName}}">
<meta property="og:url" content="{{.Repository.HTMLURL}}">
<meta property="og:description" content="{{.Repository.Description}}">
{{else if .Repository}}
{{if .Issue}}
<meta property="og:title" content="{{.Issue.Title}}">