mirror of
https://github.com/thomiceli/opengist
synced 2024-11-08 12:55:50 +01:00
Move code rendering to the backend & frontend improvements (#176)
Added Chroma & Goldmark Added Mermaidjs More languages supported Add default values for gist links input Added copy code from markdown blocks
This commit is contained in:
parent
eff88711ea
commit
845e28dd59
8
go.mod
8
go.mod
@ -3,6 +3,8 @@ module github.com/thomiceli/opengist
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.12.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/glebarez/go-sqlite v1.21.2
|
||||
github.com/glebarez/sqlite v1.9.0
|
||||
github.com/go-playground/validator/v10 v10.15.4
|
||||
@ -13,6 +15,10 @@ require (
|
||||
github.com/markbates/goth v1.78.0
|
||||
github.com/rs/zerolog v1.30.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/yuin/goldmark v1.6.0
|
||||
github.com/yuin/goldmark-emoji v1.0.2
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0
|
||||
golang.org/x/crypto v0.13.0
|
||||
golang.org/x/text v0.13.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@ -21,7 +27,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
|
29
go.sum
29
go.sum
@ -34,7 +34,16 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
|
||||
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY=
|
||||
github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@ -46,6 +55,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@ -68,6 +81,9 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
|
||||
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
||||
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
@ -151,12 +167,14 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@ -177,6 +195,7 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
|
||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
|
||||
github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
|
||||
@ -220,7 +239,17 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
|
||||
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38=
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
|
@ -2,11 +2,12 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/labstack/echo/v4"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -339,8 +340,16 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var size int64
|
||||
|
||||
size, err = git.GetFileSize(gist.User.Username, gist.Uuid, revision, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &git.File{
|
||||
Filename: filename,
|
||||
Size: humanize.IBytes(uint64(size)),
|
||||
Content: content,
|
||||
Truncated: truncated,
|
||||
}, err
|
||||
|
@ -149,6 +149,25 @@ func GetFileContent(user string, gist string, revision string, filename string,
|
||||
return content, truncated, nil
|
||||
}
|
||||
|
||||
func GetFileSize(user string, gist string, revision string, filename string) (int64, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command(
|
||||
"git",
|
||||
"cat-file",
|
||||
"-s",
|
||||
revision+":"+filename,
|
||||
)
|
||||
cmd.Dir = repositoryPath
|
||||
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.ParseInt(strings.TrimSuffix(string(stdout), "\n"), 10, 64)
|
||||
}
|
||||
|
||||
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
|
||||
type File struct {
|
||||
Filename string
|
||||
Size string
|
||||
OldFilename string
|
||||
Content string
|
||||
Truncated bool
|
||||
|
134
internal/render/highlight.go
Normal file
134
internal/render/highlight.go
Normal file
@ -0,0 +1,134 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
)
|
||||
|
||||
type RenderedFile struct {
|
||||
*git.File
|
||||
Type string
|
||||
Lines []string
|
||||
HTML string
|
||||
}
|
||||
|
||||
type RenderedGist struct {
|
||||
*db.Gist
|
||||
Lines []string
|
||||
HTML string
|
||||
}
|
||||
|
||||
func HighlightFile(file *git.File) (RenderedFile, error) {
|
||||
rendered := RenderedFile{
|
||||
File: file,
|
||||
}
|
||||
|
||||
style := newStyle()
|
||||
lexer := newLexer(file.Filename)
|
||||
if lexer.Config().Name == "markdown" {
|
||||
return MarkdownFile(file)
|
||||
}
|
||||
|
||||
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
|
||||
|
||||
iterator, err := lexer.Tokenise(nil, file.Content)
|
||||
if err != nil {
|
||||
return rendered, err
|
||||
}
|
||||
|
||||
htmlbuf := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&htmlbuf)
|
||||
|
||||
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
|
||||
lines := make([]string, 0, len(tokensLines))
|
||||
for _, tokens := range tokensLines {
|
||||
iterator = chroma.Literator(tokens...)
|
||||
err = formatter.Format(&htmlbuf, style, iterator)
|
||||
if err != nil {
|
||||
return rendered, fmt.Errorf("unable to format code: %w", err)
|
||||
}
|
||||
lines = append(lines, htmlbuf.String())
|
||||
htmlbuf.Reset()
|
||||
}
|
||||
|
||||
_ = w.Flush()
|
||||
|
||||
rendered.Lines = lines
|
||||
rendered.Type = parseFileTypeName(*lexer.Config())
|
||||
|
||||
return rendered, err
|
||||
}
|
||||
|
||||
func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
|
||||
rendered := RenderedGist{
|
||||
Gist: gist,
|
||||
}
|
||||
|
||||
style := newStyle()
|
||||
lexer := newLexer(gist.PreviewFilename)
|
||||
if lexer.Config().Name == "markdown" {
|
||||
return MarkdownGistPreview(gist)
|
||||
}
|
||||
|
||||
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
|
||||
|
||||
iterator, err := lexer.Tokenise(nil, gist.Preview)
|
||||
if err != nil {
|
||||
return rendered, err
|
||||
}
|
||||
|
||||
htmlbuf := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&htmlbuf)
|
||||
|
||||
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
|
||||
lines := make([]string, 0, len(tokensLines))
|
||||
for _, tokens := range tokensLines {
|
||||
iterator = chroma.Literator(tokens...)
|
||||
err = formatter.Format(&htmlbuf, style, iterator)
|
||||
if err != nil {
|
||||
return rendered, fmt.Errorf("unable to format code: %w", err)
|
||||
}
|
||||
lines = append(lines, htmlbuf.String())
|
||||
htmlbuf.Reset()
|
||||
}
|
||||
|
||||
_ = w.Flush()
|
||||
|
||||
rendered.Lines = lines
|
||||
|
||||
return rendered, err
|
||||
}
|
||||
|
||||
func parseFileTypeName(config chroma.Config) string {
|
||||
fileType := config.Name
|
||||
if fileType == "fallback" || fileType == "plaintext" {
|
||||
return "Text"
|
||||
}
|
||||
|
||||
return fileType
|
||||
}
|
||||
|
||||
func newLexer(filename string) chroma.Lexer {
|
||||
var lexer chroma.Lexer
|
||||
if lexer = lexers.Get(filename); lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
return lexer
|
||||
}
|
||||
|
||||
func newStyle() *chroma.Style {
|
||||
var style *chroma.Style
|
||||
if style = styles.Get("catppuccin-latte"); style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
47
internal/render/markdown.go
Normal file
47
internal/render/markdown.go
Normal file
@ -0,0 +1,47 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/yuin/goldmark"
|
||||
emoji "github.com/yuin/goldmark-emoji"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"go.abhg.dev/goldmark/mermaid"
|
||||
)
|
||||
|
||||
func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) {
|
||||
var buf bytes.Buffer
|
||||
err := newMarkdown().Convert([]byte(gist.Preview), &buf)
|
||||
|
||||
return RenderedGist{
|
||||
Gist: gist,
|
||||
HTML: buf.String(),
|
||||
}, err
|
||||
}
|
||||
|
||||
func MarkdownFile(file *git.File) (RenderedFile, error) {
|
||||
var buf bytes.Buffer
|
||||
err := newMarkdown().Convert([]byte(file.Content), &buf)
|
||||
|
||||
return RenderedFile{
|
||||
File: file,
|
||||
HTML: buf.String(),
|
||||
Type: "Markdown",
|
||||
}, err
|
||||
}
|
||||
|
||||
func newMarkdown() goldmark.Markdown {
|
||||
return goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle("catppuccin-latte"),
|
||||
highlighting.WithFormatOptions(html.WithClasses(true))),
|
||||
emoji.Emoji,
|
||||
&mermaid.Extender{},
|
||||
),
|
||||
)
|
||||
}
|
@ -4,6 +4,8 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/render"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"regexp"
|
||||
@ -232,11 +234,20 @@ func allGists(ctx echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
renderedFiles := make([]*render.RenderedGist, 0, len(gists))
|
||||
for _, gist := range gists {
|
||||
rendered, err := render.HighlightGistPreview(gist)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error rendering gist preview for " + gist.Uuid + " - " + gist.PreviewFilename)
|
||||
}
|
||||
renderedFiles = append(renderedFiles, &rendered)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching gists", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, gists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
|
||||
if err = paginate(ctx, renderedFiles, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
}
|
||||
|
||||
@ -261,9 +272,18 @@ func gistIndex(ctx echo.Context) error {
|
||||
return notFound("Revision not found")
|
||||
}
|
||||
|
||||
renderedFiles := make([]render.RenderedFile, 0, len(files))
|
||||
for _, file := range files {
|
||||
rendered, err := render.HighlightFile(file)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error rendering gist preview for " + gist.Uuid + " - " + gist.PreviewFilename)
|
||||
}
|
||||
renderedFiles = append(renderedFiles, rendered)
|
||||
}
|
||||
|
||||
setData(ctx, "page", "code")
|
||||
setData(ctx, "commit", revision)
|
||||
setData(ctx, "files", files)
|
||||
setData(ctx, "files", renderedFiles)
|
||||
setData(ctx, "revision", revision)
|
||||
setData(ctx, "htmlTitle", gist.Title)
|
||||
return html(ctx, "gist.html")
|
||||
|
@ -117,6 +117,9 @@ var (
|
||||
"toStr": func(i interface{}) string {
|
||||
return fmt.Sprint(i)
|
||||
},
|
||||
"safe": func(s string) template.HTML {
|
||||
return template.HTML(s)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
74
package-lock.json
generated
74
package-lock.json
generated
@ -20,9 +20,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"cssnano": "^5.1.15",
|
||||
"dayjs": "^1.11.9",
|
||||
"github-markdown-css": "^5.2.0",
|
||||
"highlight.js": "^11.7.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"github-markdown-css": "^5.5.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss-cssnext": "^3.1.1",
|
||||
@ -614,9 +612,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.10.tgz",
|
||||
"integrity": "sha512-XJu3fZjHVVjJcRS7kHdwBO50irXc4H8rQwgm6SmT3Y8IHWk7WzpaLsaR2vdr/jSk/J4pQhXc1WLul7jVdxC+0Q==",
|
||||
"version": "1.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.11.tgz",
|
||||
"integrity": "sha512-B5Y9EJF4BWiMgj4ufxUo2hrORnmMBDrMtR+L7dwIO5pocuSAahG6QBwXR6PbKJOjRywJczU2r2LJPg79ER91TQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
@ -1591,15 +1589,6 @@
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer/node_modules/entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
@ -1662,13 +1651,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
@ -1980,15 +1966,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.9.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz",
|
||||
"integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
@ -2204,15 +2181,6 @@
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
|
||||
@ -2290,22 +2258,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
|
||||
"integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~3.0.1",
|
||||
"linkify-it": "^4.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.js"
|
||||
}
|
||||
},
|
||||
"node_modules/math-expression-evaluator": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz",
|
||||
@ -2318,12 +2270,6 @@
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@ -4880,12 +4826,6 @@
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
|
@ -20,9 +20,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"cssnano": "^5.1.15",
|
||||
"dayjs": "^1.11.9",
|
||||
"github-markdown-css": "^5.2.0",
|
||||
"highlight.js": "^11.7.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"github-markdown-css": "^5.5.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss-cssnext": "^3.1.1",
|
||||
|
73
public/catppuccin-latte.css
vendored
Normal file
73
public/catppuccin-latte.css
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
/* Error */ .chroma .err { color: #d20f39 }
|
||||
/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
|
||||
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
|
||||
/* LineHighlight */ .chroma .hl { color: #bcc0cc }
|
||||
/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8c8fa1 }
|
||||
/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8c8fa1 }
|
||||
/* Line */ .chroma .line { display: flex; }
|
||||
/* Keyword */ .chroma .k { color: #8839ef }
|
||||
/* KeywordConstant */ .chroma .kc { color: #fe640b }
|
||||
/* KeywordDeclaration */ .chroma .kd { color: #d20f39 }
|
||||
/* KeywordNamespace */ .chroma .kn { color: #179299 }
|
||||
/* KeywordPseudo */ .chroma .kp { color: #8839ef }
|
||||
/* KeywordReserved */ .chroma .kr { color: #8839ef }
|
||||
/* KeywordType */ .chroma .kt { color: #d20f39 }
|
||||
/* NameAttribute */ .chroma .na { color: #1e66f5 }
|
||||
/* NameBuiltin */ .chroma .nb { color: #04a5e5 }
|
||||
/* NameBuiltinPseudo */ .chroma .bp { color: #04a5e5 }
|
||||
/* NameClass */ .chroma .nc { color: #df8e1d }
|
||||
/* NameConstant */ .chroma .no { color: #df8e1d }
|
||||
/* NameDecorator */ .chroma .nd { color: #1e66f5; font-weight: bold }
|
||||
/* NameEntity */ .chroma .ni { color: #179299 }
|
||||
/* NameException */ .chroma .ne { color: #fe640b }
|
||||
/* NameFunction */ .chroma .nf { color: #1e66f5 }
|
||||
/* NameFunctionMagic */ .chroma .fm { color: #1e66f5 }
|
||||
/* NameLabel */ .chroma .nl { color: #04a5e5 }
|
||||
/* NameNamespace */ .chroma .nn { color: #fe640b }
|
||||
/* NameProperty */ .chroma .py { color: #fe640b }
|
||||
/* NameTag */ .chroma .nt { color: #8839ef }
|
||||
/* NameVariable */ .chroma .nv { color: #dc8a78 }
|
||||
/* NameVariableClass */ .chroma .vc { color: #dc8a78 }
|
||||
/* NameVariableGlobal */ .chroma .vg { color: #dc8a78 }
|
||||
/* NameVariableInstance */ .chroma .vi { color: #dc8a78 }
|
||||
/* NameVariableMagic */ .chroma .vm { color: #dc8a78 }
|
||||
/* LiteralString */ .chroma .s { color: #40a02b }
|
||||
/* LiteralStringAffix */ .chroma .sa { color: #d20f39 }
|
||||
/* LiteralStringBacktick */ .chroma .sb { color: #40a02b }
|
||||
/* LiteralStringChar */ .chroma .sc { color: #40a02b }
|
||||
/* LiteralStringDelimiter */ .chroma .dl { color: #1e66f5 }
|
||||
/* LiteralStringDoc */ .chroma .sd { color: #9ca0b0 }
|
||||
/* LiteralStringDouble */ .chroma .s2 { color: #40a02b }
|
||||
/* LiteralStringEscape */ .chroma .se { color: #1e66f5 }
|
||||
/* LiteralStringHeredoc */ .chroma .sh { color: #9ca0b0 }
|
||||
/* LiteralStringInterpol */ .chroma .si { color: #40a02b }
|
||||
/* LiteralStringOther */ .chroma .sx { color: #40a02b }
|
||||
/* LiteralStringRegex */ .chroma .sr { color: #179299 }
|
||||
/* LiteralStringSingle */ .chroma .s1 { color: #40a02b }
|
||||
/* LiteralStringSymbol */ .chroma .ss { color: #40a02b }
|
||||
/* LiteralNumber */ .chroma .m { color: #fe640b }
|
||||
/* LiteralNumberBin */ .chroma .mb { color: #fe640b }
|
||||
/* LiteralNumberFloat */ .chroma .mf { color: #fe640b }
|
||||
/* LiteralNumberHex */ .chroma .mh { color: #fe640b }
|
||||
/* LiteralNumberInteger */ .chroma .mi { color: #fe640b }
|
||||
/* LiteralNumberIntegerLong */ .chroma .il { color: #fe640b }
|
||||
/* LiteralNumberOct */ .chroma .mo { color: #fe640b }
|
||||
/* Operator */ .chroma .o { color: #04a5e5; font-weight: bold }
|
||||
/* OperatorWord */ .chroma .ow { color: #04a5e5; font-weight: bold }
|
||||
/* Comment */ .chroma .c { color: #9ca0b0; font-style: italic }
|
||||
/* CommentHashbang */ .chroma .ch { color: #9ca0b0; font-style: italic }
|
||||
/* CommentMultiline */ .chroma .cm { color: #9ca0b0; font-style: italic }
|
||||
/* CommentSingle */ .chroma .c1 { color: #9ca0b0; font-style: italic }
|
||||
/* CommentSpecial */ .chroma .cs { color: #9ca0b0; font-style: italic }
|
||||
/* CommentPreproc */ .chroma .cp { color: #9ca0b0; font-style: italic }
|
||||
/* CommentPreprocFile */ .chroma .cpf { color: #9ca0b0; font-weight: bold; font-style: italic }
|
||||
/* GenericDeleted */ .chroma .gd { color: #d20f39; background-color: #ccd0da }
|
||||
/* GenericEmph */ .chroma .ge { font-style: italic }
|
||||
/* GenericError */ .chroma .gr { color: #d20f39 }
|
||||
/* GenericHeading */ .chroma .gh { color: #fe640b; font-weight: bold }
|
||||
/* GenericInserted */ .chroma .gi { color: #40a02b; background-color: #ccd0da }
|
||||
/* GenericStrong */ .chroma .gs { font-weight: bold }
|
||||
/* GenericSubheading */ .chroma .gu { color: #fe640b; font-weight: bold }
|
||||
/* GenericTraceback */ .chroma .gt { color: #d20f39 }
|
||||
/* GenericUnderline */ .chroma .gl { text-decoration: underline }
|
73
public/catppuccin-macchiato.css
vendored
Normal file
73
public/catppuccin-macchiato.css
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
/* Error */ .chroma .err { color: #f38ba8 }
|
||||
/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
|
||||
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
|
||||
/* LineHighlight */ .chroma .hl { color: #45475a }
|
||||
/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f849c }
|
||||
/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f849c }
|
||||
/* Line */ .chroma .line { display: flex; }
|
||||
/* Keyword */ .chroma .k { color: #cba6f7 }
|
||||
/* KeywordConstant */ .chroma .kc { color: #fab387 }
|
||||
/* KeywordDeclaration */ .chroma .kd { color: #f38ba8 }
|
||||
/* KeywordNamespace */ .chroma .kn { color: #94e2d5 }
|
||||
/* KeywordPseudo */ .chroma .kp { color: #cba6f7 }
|
||||
/* KeywordReserved */ .chroma .kr { color: #cba6f7 }
|
||||
/* KeywordType */ .chroma .kt { color: #f38ba8 }
|
||||
/* NameAttribute */ .chroma .na { color: #89b4fa }
|
||||
/* NameBuiltin */ .chroma .nb { color: #89dceb }
|
||||
/* NameBuiltinPseudo */ .chroma .bp { color: #89dceb }
|
||||
/* NameClass */ .chroma .nc { color: #f9e2af }
|
||||
/* NameConstant */ .chroma .no { color: #f9e2af }
|
||||
/* NameDecorator */ .chroma .nd { color: #89b4fa; font-weight: bold }
|
||||
/* NameEntity */ .chroma .ni { color: #94e2d5 }
|
||||
/* NameException */ .chroma .ne { color: #fab387 }
|
||||
/* NameFunction */ .chroma .nf { color: #89b4fa }
|
||||
/* NameFunctionMagic */ .chroma .fm { color: #89b4fa }
|
||||
/* NameLabel */ .chroma .nl { color: #89dceb }
|
||||
/* NameNamespace */ .chroma .nn { color: #fab387 }
|
||||
/* NameProperty */ .chroma .py { color: #fab387 }
|
||||
/* NameTag */ .chroma .nt { color: #cba6f7 }
|
||||
/* NameVariable */ .chroma .nv { color: #f5e0dc }
|
||||
/* NameVariableClass */ .chroma .vc { color: #f5e0dc }
|
||||
/* NameVariableGlobal */ .chroma .vg { color: #f5e0dc }
|
||||
/* NameVariableInstance */ .chroma .vi { color: #f5e0dc }
|
||||
/* NameVariableMagic */ .chroma .vm { color: #f5e0dc }
|
||||
/* LiteralString */ .chroma .s { color: #a6e3a1 }
|
||||
/* LiteralStringAffix */ .chroma .sa { color: #f38ba8 }
|
||||
/* LiteralStringBacktick */ .chroma .sb { color: #a6e3a1 }
|
||||
/* LiteralStringChar */ .chroma .sc { color: #a6e3a1 }
|
||||
/* LiteralStringDelimiter */ .chroma .dl { color: #89b4fa }
|
||||
/* LiteralStringDoc */ .chroma .sd { color: #6c7086 }
|
||||
/* LiteralStringDouble */ .chroma .s2 { color: #a6e3a1 }
|
||||
/* LiteralStringEscape */ .chroma .se { color: #89b4fa }
|
||||
/* LiteralStringHeredoc */ .chroma .sh { color: #6c7086 }
|
||||
/* LiteralStringInterpol */ .chroma .si { color: #a6e3a1 }
|
||||
/* LiteralStringOther */ .chroma .sx { color: #a6e3a1 }
|
||||
/* LiteralStringRegex */ .chroma .sr { color: #94e2d5 }
|
||||
/* LiteralStringSingle */ .chroma .s1 { color: #a6e3a1 }
|
||||
/* LiteralStringSymbol */ .chroma .ss { color: #a6e3a1 }
|
||||
/* LiteralNumber */ .chroma .m { color: #fab387 }
|
||||
/* LiteralNumberBin */ .chroma .mb { color: #fab387 }
|
||||
/* LiteralNumberFloat */ .chroma .mf { color: #fab387 }
|
||||
/* LiteralNumberHex */ .chroma .mh { color: #fab387 }
|
||||
/* LiteralNumberInteger */ .chroma .mi { color: #fab387 }
|
||||
/* LiteralNumberIntegerLong */ .chroma .il { color: #fab387 }
|
||||
/* LiteralNumberOct */ .chroma .mo { color: #fab387 }
|
||||
/* Operator */ .chroma .o { color: #89dceb; font-weight: bold }
|
||||
/* OperatorWord */ .chroma .ow { color: #89dceb; font-weight: bold }
|
||||
/* Comment */ .chroma .c { color: #6c7086; font-style: italic }
|
||||
/* CommentHashbang */ .chroma .ch { color: #6c7086; font-style: italic }
|
||||
/* CommentMultiline */ .chroma .cm { color: #6c7086; font-style: italic }
|
||||
/* CommentSingle */ .chroma .c1 { color: #6c7086; font-style: italic }
|
||||
/* CommentSpecial */ .chroma .cs { color: #6c7086; font-style: italic }
|
||||
/* CommentPreproc */ .chroma .cp { color: #6c7086; font-style: italic }
|
||||
/* CommentPreprocFile */ .chroma .cpf { color: #6c7086; font-weight: bold; font-style: italic }
|
||||
/* GenericDeleted */ .chroma .gd { color: #f38ba8; background-color: #313244 }
|
||||
/* GenericEmph */ .chroma .ge { font-style: italic }
|
||||
/* GenericError */ .chroma .gr { color: #f38ba8 }
|
||||
/* GenericHeading */ .chroma .gh { color: #fab387; font-weight: bold }
|
||||
/* GenericInserted */ .chroma .gi { color: #a6e3a1; background-color: #313244 }
|
||||
/* GenericStrong */ .chroma .gs { font-weight: bold }
|
||||
/* GenericSubheading */ .chroma .gu { color: #fab387; font-weight: bold }
|
||||
/* GenericTraceback */ .chroma .gt { color: #f38ba8 }
|
||||
/* GenericUnderline */ .chroma .gl { text-decoration: underline }
|
38
public/gist.ts
Normal file
38
public/gist.ts
Normal file
@ -0,0 +1,38 @@
|
||||
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
|
||||
el.addEventListener('click', event => {
|
||||
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
|
||||
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
||||
|
||||
const nextSibling = (event.target as HTMLElement).nextSibling;
|
||||
if (nextSibling instanceof HTMLElement) {
|
||||
nextSibling.classList.add('selected');
|
||||
}
|
||||
|
||||
const filename = el.dataset.filenameSlug;
|
||||
const line = (event.target as HTMLElement).textContent;
|
||||
const url = location.protocol + '//' + location.host + location.pathname;
|
||||
const hash = '#file-' + filename + '-' + line;
|
||||
window.history.pushState(null, null, url + hash);
|
||||
location.hash = hash;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let copybtnhtml = `<button type="button" style="top: 1em !important; right: 1em !important;" class="md-code-copy-btn absolute focus-within:z-auto rounded-md dark:border-gray-600 px-2 py-2 opacity-80 font-medium text-slate-700 bg-gray-100 dark:bg-gray-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" /></svg></button>`;
|
||||
|
||||
document.querySelectorAll<HTMLElement>('.markdown-body pre').forEach((el) => {
|
||||
el.innerHTML = copybtnhtml + `<span class="code-div">` + el.innerHTML + `</span>`;
|
||||
});
|
||||
|
||||
document.querySelectorAll('.md-code-copy-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
let code = this.nextElementSibling.textContent;
|
||||
navigator.clipboard.writeText(code).catch((err) => {
|
||||
console.error('Could not copy text: ', err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
@ -1,49 +0,0 @@
|
||||
import hljs from 'highlight.js';
|
||||
import md from 'markdown-it';
|
||||
|
||||
document.querySelectorAll('.markdown').forEach((e: HTMLElement) => {
|
||||
e.innerHTML = md({
|
||||
html: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return '<pre class="hljs"><code>' +
|
||||
hljs.highlight(str, {language: lang, ignoreIllegals: true}).value +
|
||||
'</code></pre>';
|
||||
} catch (__) {
|
||||
}
|
||||
}
|
||||
|
||||
return '<pre class="hljs"><code>' + md().utils.escapeHtml(str) + '</code></pre>';
|
||||
}
|
||||
}).render(e.textContent);
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
|
||||
const ext = el.dataset.filename?.split('.').pop() || '';
|
||||
if (hljs.getLanguage(ext) && ext !== 'txt') {
|
||||
el.querySelectorAll<HTMLElement>('td.line-code').forEach((ell) => {
|
||||
ell.classList.add('language-' + ext);
|
||||
hljs.highlightElement(ell);
|
||||
});
|
||||
}
|
||||
|
||||
el.addEventListener('click', event => {
|
||||
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
|
||||
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
||||
|
||||
const nextSibling = (event.target as HTMLElement).nextSibling;
|
||||
if (nextSibling instanceof HTMLElement) {
|
||||
nextSibling.classList.add('selected');
|
||||
}
|
||||
|
||||
|
||||
const filename = el.dataset.filenameSlug;
|
||||
const line = (event.target as HTMLElement).textContent;
|
||||
const url = location.protocol + '//' + location.host + location.pathname;
|
||||
const hash = '#file-' + filename + '-' + line;
|
||||
window.history.pushState(null, null, url + hash);
|
||||
location.hash = hash;
|
||||
}
|
||||
});
|
||||
});
|
20
public/style.css
vendored
20
public/style.css
vendored
@ -156,3 +156,23 @@ dl.dl-config dd {
|
||||
.markdown-body {
|
||||
@apply dark:bg-gray-900 !important;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
@apply flex relative items-start p-0 !important;
|
||||
}
|
||||
|
||||
.markdown-body .code-div {
|
||||
@apply p-4 max-w-full overflow-x-auto !important;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
@apply overflow-auto whitespace-pre !important;
|
||||
}
|
||||
|
||||
.chroma.preview.markdown code {
|
||||
@apply p-4 !important;
|
||||
}
|
||||
|
||||
.mermaid {
|
||||
background: #f6f8fa !important;
|
||||
}
|
||||
|
4
public/style.scss
vendored
4
public/style.scss
vendored
@ -1,9 +1,9 @@
|
||||
:root {
|
||||
@import "github-markdown-css/github-markdown-light";
|
||||
@import 'highlight.js/scss/base16/one-light.scss';
|
||||
@import './catppuccin-latte';
|
||||
}
|
||||
|
||||
.dark {
|
||||
@import "github-markdown-css/github-markdown-dark";
|
||||
@import 'highlight.js/scss/base16/onedark.scss';
|
||||
@import './catppuccin-macchiato';
|
||||
}
|
||||
|
3
templates/base/base_footer.html
vendored
3
templates/base/base_footer.html
vendored
@ -30,9 +30,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="{{ asset "hljs.ts" }}"></script>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
1
templates/base/gist_footer.html
vendored
1
templates/base/gist_footer.html
vendored
@ -4,6 +4,7 @@
|
||||
{{ end }}
|
||||
|
||||
{{ define "gist_footer" }}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
7
templates/base/gist_header.html
vendored
7
templates/base/gist_header.html
vendored
@ -94,7 +94,7 @@
|
||||
<p class="mt-1 max-w-2xl text-sm text-slate-500">{{ .locale.Tr "gist.header.last-active" }} <span class="moment-timestamp"> {{ .gist.UpdatedAt }} </span>
|
||||
{{ if .gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr .gist.Private false }} </span>{{ end }}
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-slate-700 dark:text-slate-300">{{ .gist.Description }}</p>
|
||||
<p class="mt-1 text-sm max-w-2xl text-slate-600 dark:text-slate-400">{{ .gist.Description }}</p>
|
||||
</header>
|
||||
<main class="mt-4">
|
||||
|
||||
@ -128,7 +128,7 @@
|
||||
<div class="flex rounded-md shadow-sm">
|
||||
<div class="relative">
|
||||
<button type="button" id="gist-menu-toggle" class="relative text-xs inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3 focus-within:z-10 -mr-px">
|
||||
<span id="gist-menu-title" class="whitespace-nowrap"></span>
|
||||
<span id="gist-menu-title" class="whitespace-nowrap">{{if .httpCloneUrl}}{{ .locale.Tr "gist.header.clone-http" .httpProtocol }}{{ else if .sshCloneUrl }}{{ .locale.Tr "gist.header.clone-ssh" }}{{else}}{{ .locale.Tr "gist.header.share" }}{{end}}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
@ -152,13 +152,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex flex-grow items-stretch focus-within:z-10">
|
||||
<input id="gist-menu-input" value="" class="block code bg-white dark:bg-gray-900 w-full rounded-none border border-gray-200 dark:border-gray-600 focus:border-primary-500 focus:ring-primary-500 focus:outline-none focus:ring-1 text-xs px-2">
|
||||
<input id="gist-menu-input" value="{{if .httpCloneUrl}}{{.httpCloneUrl}}{{ else if .sshCloneUrl }}{{.sshCloneUrl}}{{else}}{{.httpCopyUrl}}{{end}}" class="block code bg-white dark:bg-gray-900 w-full rounded-none border border-gray-200 dark:border-gray-600 focus:border-primary-500 focus:ring-primary-500 focus:outline-none focus:ring-1 text-xs px-2 py-1">
|
||||
</div>
|
||||
<button id="gist-menu-button-copy" type="button" class="relative text-xs -ml-px inline-flex items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
8
templates/pages/all.html
vendored
8
templates/pages/all.html
vendored
@ -152,17 +152,17 @@
|
||||
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto hover:border-primary-600">
|
||||
<div class="code overflow-auto">
|
||||
{{ if isMarkdown $gist.PreviewFilename }}
|
||||
<div class="markdown markdown-body p-8">{{ $gist.Preview }}</div>
|
||||
<div class="chroma preview markdown markdown-body p-8">{{ $gist.HTML | safe }}</div>
|
||||
{{ else }}
|
||||
<table class="table-code w-full whitespace-pre" data-filename="{{ $gist.PreviewFilename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
|
||||
<table class="chroma table-code w-full whitespace-pre" data-filename="{{ $gist.PreviewFilename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
|
||||
<tbody>
|
||||
{{ $ii := "1" }}
|
||||
{{ $i := toInt $ii }}
|
||||
{{ range $line := lines $gist.Preview }}
|
||||
{{ range $line := $gist.Lines }}
|
||||
|
||||
<tr>
|
||||
<td class="select-none line-num px-4">{{$i}}</td>
|
||||
<td class="line-code">{{ $line }}</td>
|
||||
<td class="line-code">{{ $line | safe }}</td>
|
||||
</tr>
|
||||
{{ $i = inc $i }}
|
||||
{{ end }}
|
||||
|
20
templates/pages/gist.html
vendored
20
templates/pages/gist.html
vendored
@ -3,7 +3,7 @@
|
||||
{{ if .files }}
|
||||
<div class="grid gap-y-4">
|
||||
{{ range $file := .files }}
|
||||
{{ $csv := csvFile $file }}
|
||||
{{ $csv := csvFile $file.File }}
|
||||
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto">
|
||||
<div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto block">
|
||||
<div class="ml-4 py-1.5 flex">
|
||||
@ -12,7 +12,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-700 dark:text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
<a href="{{ $.c.ExternalUrl }}#file-{{ slug $file.Filename }}" class="text-slate-700 dark:text-slate-300 hover:text-black dark:hover:text-white ml-2">{{ $file.Filename }}</a></span>
|
||||
<a href="{{ $.c.ExternalUrl }}#file-{{ slug $file.Filename }}" class="hover:text-primary-600 ml-2 mr-1">{{ $file.Filename }}</a>
|
||||
<span class="hidden sm:block">
|
||||
<span class="text-gray-400"> · {{ $file.Size }} · {{ $file.Type }}</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="isolate inline-flex rounded-md shadow-sm mr-2">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/raw/{{ $.commit }}/{{$file.Filename}}" class="relative inline-flex items-center rounded-l-md bg-white text-gray-500 dark:text-slate-300 float-right px-2.5 py-1 leading-4 text-xs font-medium dark:bg-gray-600 border border-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-slate-700 dark:hover:text-slate-300 select-none">
|
||||
@ -63,16 +67,16 @@
|
||||
{{ end }}
|
||||
</table>
|
||||
{{ else if isMarkdown $file.Filename }}
|
||||
<div class="markdown markdown-body p-8">{{ $file.Content }}</div>
|
||||
<div class="chroma markdown markdown-body p-8">{{ $file.HTML | safe }}</div>
|
||||
{{ else }}
|
||||
<div class="code">
|
||||
{{ $fileslug := slug $file.Filename }}
|
||||
{{ if ne $file.Content "" }}
|
||||
<table class="table-code w-full whitespace-pre" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
|
||||
<table class="chroma table-code w-full whitespace-pre" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
|
||||
<tbody>
|
||||
{{ $ii := "1" }}
|
||||
{{ $i := toInt $ii }}
|
||||
{{ range $line := lines $file.Content }}<tr><td id="file-{{ $fileslug }}-{{$i}}" class="select-none line-num px-4">{{$i}}</td><td class="line-code">{{ $line }}</td></tr>{{ $i = inc $i }}{{ end }}
|
||||
{{ range $line := $file.Lines }}<tr><td id="file-{{ $fileslug }}-{{$i}}" class="select-none line-num px-4">{{$i}}</td><td class="line-code">{{ $line | safe }}</td></tr>{{ $i = inc $i }}{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
@ -90,5 +94,11 @@
|
||||
<h3 class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "gist.no-content" }}</h3>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- make sure tailwind knows those classes -->
|
||||
<button type="button" style="top: 1em !important; right: 1em !important;" class="hidden md-code-copy-btn absolute right-0 top-0 focus-within:z-auto rounded-md dark:border-gray-600 px-2 py-2 opacity-80 font-medium text-slate-700 bg-gray-100 dark:bg-gray-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" /></svg></button>
|
||||
|
||||
<script type="module" src="{{ asset "gist.ts" }}"></script>
|
||||
|
||||
{{ template "gist_footer" .}}
|
||||
{{ template "footer" .}}
|
||||
|
2
templates/pages/revisions.html
vendored
2
templates/pages/revisions.html
vendored
@ -50,7 +50,7 @@
|
||||
{{ else if eq $file.Content "" }}
|
||||
<p class="m-2 ml-4 text-sm">{{ $.locale.Tr "gist.revision.empty-file" }}</p>
|
||||
{{ else }}
|
||||
<table class="code table-code w-full whitespace-pre" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0">
|
||||
<table class="code chroma table-code w-full whitespace-pre" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0">
|
||||
<tbody>
|
||||
{{ $left := 0 }}
|
||||
{{ $right := 0 }}
|
||||
|
2
vite.config.js
vendored
2
vite.config.js
vendored
@ -9,7 +9,7 @@ export default defineConfig({
|
||||
assetsDir: 'assets',
|
||||
manifest: true,
|
||||
rollupOptions: {
|
||||
input: ['./public/main.ts', './public/editor.ts', './public/admin.ts', './public/hljs.ts']
|
||||
input: ['./public/main.ts', './public/editor.ts', './public/admin.ts', './public/gist.ts']
|
||||
},
|
||||
assetsInlineLimit: 0,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user