Compare commits

...

4 Commits

12 changed files with 1614 additions and 2 deletions

@ -624,11 +624,21 @@
"path": "github.com/jbenet/go-context/io/LICENSE",
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Juan Batiz-Benet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
},
{
"name": "github.com/jbuchbinder/gg",
"path": "github.com/jbuchbinder/gg/LICENSE.md",
"licenseText": "Copyright (C) 2016 Michael Fogleman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/jhillyerd/enmime",
"path": "github.com/jhillyerd/enmime/LICENSE",
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2012-2016 James Hillyerd, All Rights Reserved\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
},
{
"name": "github.com/jiro4989/textimg/v3",
"path": "github.com/jiro4989/textimg/v3/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2019 jiro4989\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/josharian/intern",
"path": "github.com/josharian/intern/license.md",

@ -125,6 +125,8 @@ require (
xorm.io/xorm v1.3.3-0.20230623150031-18f8e7a86c75
)
require github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
require (
cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
@ -214,7 +216,9 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.15 // 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.6 // indirect

@ -512,6 +512,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-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -744,10 +746,14 @@ github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQykt
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/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.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.11.1 h1:U6ToGVxfxNQQhKrAaGxtwOf7Zqksb8AQ3j1CyAWOk5k=
github.com/jhillyerd/enmime v0.11.1/go.mod h1:EktNOa/V6ka9yCrfoB2uxgefp1lno6OVdszW0iQ5LnM=
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=

@ -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"
@ -197,6 +198,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))

@ -28,6 +28,8 @@ import (
"code.gitea.io/gitea/modules/actions"
"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"
@ -525,6 +527,13 @@ 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
@ -715,6 +724,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.Other.EnableFeed {

@ -1348,6 +1348,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)

@ -1,6 +1,6 @@
<footer class="page-footer" role="group" aria-label="{{.locale.Tr "aria.footer"}}">
<div class="left-links" role="contentinfo" aria-label="{{.locale.Tr "aria.footer.software"}}">
<a target="_blank" rel="noopener noreferrer" href="https://gitea.io">{{.locale.Tr "powered_by" "Gitea"}}</a>
<a target="_blank" rel="noopener noreferrer" href="https://gitea.pid1.sh/sanctureplicum/gitea">{{.locale.Tr "powered_by" "Sanctureplicum (Gitea fork)"}}</a>
{{if (or .ShowFooterVersion .PageIsAdmin)}}
{{.locale.Tr "version"}}:
{{if .IsAdmin}}

@ -33,6 +33,25 @@
{{if .ContextUser.Description}}
<meta property="og:description" content="{{.ContextUser.Description}}">
{{end}}
{{else if and .PageIsViewCode .HideRepoInfo }}
<meta name="twitter:site" content="@gitea">
<meta name="twitter:description" content="{{.Repository.Description}}">
<meta name="twitter:title" content="{{.Repository.Name}}/{{.FileName}}">
<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}}">
{{ if .OgImage }}
<meta property="og:description" content="{{.Repository.Description}}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image:src" content="{{.OgImage}}">
<meta property="og:image" content="{{.OgImage}}">
<meta property="og:image:alt" content="{{.Repository.Description}}">
{{ else if (.Repository.AvatarLink $.Context)}}
<meta property="og:image" content="{{.Repository.AvatarLink $.Context}}">
{{else}}
<meta property="og:image" content="{{.Repository.Owner.AvatarLink $.Context}}">
{{ end }}
{{else if .Repository}}
{{if .Issue}}
<meta property="og:title" content="{{.Issue.Title}}">
@ -41,7 +60,7 @@
<meta property="og:description" content="{{.Issue.Content}}">
{{end}}
{{else}}
<meta property="og:title" content="{{.Repository.Name}}">
<meta property="og:title" content="{{.Repository.Owner.Name}}/{{.Repository.Name}}">
<meta property="og:url" content="{{.Repository.HTMLURL}}">
{{if .Repository.Description}}
<meta property="og:description" content="{{.Repository.Description}}">