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:
Thomas Miceli
2023-12-19 02:47:14 +01:00
parent eff88711ea
commit 845e28dd59
24 changed files with 511 additions and 143 deletions

View File

@ -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

View File

@ -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)

View File

@ -12,6 +12,7 @@ import (
type File struct {
Filename string
Size string
OldFilename string
Content string
Truncated bool

View 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
}

View 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{},
),
)
}

View File

@ -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")

View File

@ -117,6 +117,9 @@ var (
"toStr": func(i interface{}) string {
return fmt.Sprint(i)
},
"safe": func(s string) template.HTML {
return template.HTML(s)
},
}
)