mirror of
https://github.com/thomiceli/opengist.git
synced 2025-06-13 22:07:11 +02: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:
@ -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)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
Reference in New Issue
Block a user