Add binary files support (#503)

This commit is contained in:
Thomas Miceli
2025-09-16 01:35:54 +02:00
committed by GitHub
parent 905276f24b
commit 594d876ba8
25 changed files with 426 additions and 194 deletions

2
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/alecthomas/chroma/v2 v2.20.0
github.com/blevesearch/bleve/v2 v2.5.0
github.com/dustin/go-humanize v1.0.1
github.com/gabriel-vasile/mimetype v1.4.8
github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-playground/validator/v10 v10.26.0
@@ -66,7 +67,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect

View File

@@ -418,12 +418,20 @@ func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
var files []*git.File
for _, fileCat := range filesCat {
var shortContent string
if len(fileCat.Content) > 512 {
shortContent = fileCat.Content[:512]
} else {
shortContent = fileCat.Content
}
files = append(files, &git.File{
Filename: fileCat.Name,
Size: fileCat.Size,
HumanSize: humanize.IBytes(fileCat.Size),
Content: fileCat.Content,
Truncated: fileCat.Truncated,
MimeType: git.DetectMimeType([]byte(shortContent)),
})
}
return files, err
@@ -444,12 +452,20 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
return nil, err
}
var shortContent string
if len(content) > 512 {
shortContent = content[:512]
} else {
shortContent = content
}
return &git.File{
Filename: filename,
Size: size,
HumanSize: humanize.IBytes(size),
Content: content,
Truncated: truncated,
MimeType: git.DetectMimeType([]byte(shortContent)),
}, err
}
@@ -660,10 +676,15 @@ func (gist *Gist) ToDTO() (*GistDTO, error) {
fileDTOs := make([]FileDTO, 0, len(files))
for _, file := range files {
fileDTOs = append(fileDTOs, FileDTO{
f := FileDTO{
Filename: file.Filename,
Content: file.Content,
})
}
if file.MimeType.CanBeEdited() {
f.Content = file.Content
} else {
f.Binary = true
}
fileDTOs = append(fileDTOs, f)
}
return &GistDTO{
@@ -702,6 +723,7 @@ type VisibilityDTO struct {
type FileDTO struct {
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"`
Content string `validate:"required"`
Binary bool
}
func (dto *GistDTO) ToGist() *Gist {

89
internal/git/mime.go Normal file
View File

@@ -0,0 +1,89 @@
package git
import (
"fmt"
"strings"
"github.com/gabriel-vasile/mimetype"
)
type MimeType struct {
ContentType string
}
func (mt MimeType) IsText() bool {
return strings.Contains(mt.ContentType, "text/")
}
func (mt MimeType) IsCSV() bool {
return strings.Contains(mt.ContentType, "text/csv")
}
func (mt MimeType) IsImage() bool {
return strings.Contains(mt.ContentType, "image/")
}
func (mt MimeType) IsSVG() bool {
return strings.Contains(mt.ContentType, "image/svg+xml")
}
func (mt MimeType) IsPDF() bool {
return strings.Contains(mt.ContentType, "application/pdf")
}
func (mt MimeType) IsAudio() bool {
return strings.Contains(mt.ContentType, "audio/")
}
func (mt MimeType) IsVideo() bool {
return strings.Contains(mt.ContentType, "video/")
}
func (mt MimeType) CanBeHighlighted() bool {
return mt.IsText() && !mt.IsCSV()
}
func (mt MimeType) CanBeEmbedded() bool {
return mt.IsImage() || mt.IsPDF() || mt.IsAudio() || mt.IsVideo()
}
func (mt MimeType) CanBeRendered() bool {
return mt.IsText() || mt.IsImage() || mt.IsSVG() || mt.IsPDF() || mt.IsAudio() || mt.IsVideo()
}
func (mt MimeType) CanBeEdited() bool {
return mt.IsText() || mt.IsSVG()
}
func (mt MimeType) RenderType() string {
t := strings.Split(mt.ContentType, "/")
str := ""
if len(t) == 2 {
str = fmt.Sprintf("(%s)", strings.ToUpper(t[1]))
}
// More user friendly description
if mt.IsImage() || mt.IsSVG() {
return fmt.Sprintf("Image %s", str)
}
if mt.IsAudio() {
return fmt.Sprintf("Audio %s", str)
}
if mt.IsVideo() {
return fmt.Sprintf("Video %s", str)
}
if mt.IsPDF() {
return "PDF"
}
if mt.IsCSV() {
return "CSV"
}
if mt.IsText() {
return "Text"
}
return "Binary"
}
func DetectMimeType(data []byte) MimeType {
return MimeType{mimetype.Detect(data).String()}
}

View File

@@ -3,27 +3,23 @@ package git
import (
"bufio"
"bytes"
"encoding/csv"
"fmt"
"io"
"regexp"
"strings"
)
type File struct {
Filename string `json:"filename"`
Size uint64 `json:"size"`
HumanSize string `json:"human_size"`
OldFilename string `json:"-"`
Content string `json:"content"`
Truncated bool `json:"truncated"`
IsCreated bool `json:"-"`
IsDeleted bool `json:"-"`
}
type CsvFile struct {
File
Header []string
Rows [][]string
Filename string `json:"filename"`
Size uint64 `json:"size"`
HumanSize string `json:"human_size"`
OldFilename string `json:"-"`
Content string `json:"content"`
Truncated bool `json:"truncated"`
IsCreated bool `json:"-"`
IsDeleted bool `json:"-"`
IsBinary bool `json:"-"`
MimeType MimeType `json:"-"`
}
type Commit struct {
@@ -62,6 +58,8 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
return string(buf), truncated, nil
}
var reLogBinaryNames = regexp.MustCompile(`Binary files (.+) and (.+) differ`)
// inspired from https://github.com/go-gitea/gitea/blob/main/services/gitdiff/gitdiff.go
func parseLog(out io.Reader, maxFiles int, maxBytes int) ([]*Commit, error) {
var commits []*Commit
@@ -206,6 +204,20 @@ loopLog:
currentFile.IsCreated = true
case strings.HasPrefix(line, "deleted file"):
currentFile.IsDeleted = true
case strings.HasPrefix(line, "Binary files"):
currentFile.IsBinary = true
names := reLogBinaryNames.FindStringSubmatch(line)
if names[1][2:] != names[2][2:] {
if currentFile.IsCreated {
currentFile.Filename = convertOctalToUTF8(names[2])[2:]
}
if currentFile.IsDeleted {
currentFile.Filename = convertOctalToUTF8(names[1])[2:]
}
} else {
currentFile.OldFilename = convertOctalToUTF8(names[1])[2:]
currentFile.Filename = convertOctalToUTF8(names[2])[2:]
}
case strings.HasPrefix(line, "--- "):
name := convertOctalToUTF8(line[4 : len(line)-1])
if parseRename && currentFile.IsDeleted {
@@ -344,27 +356,3 @@ func skipToNextCommit(input *bufio.Reader) (line string, err error) {
}
return line, err
}
func ParseCsv(file *File) (*CsvFile, error) {
reader := csv.NewReader(strings.NewReader(file.Content))
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
header := records[0]
numColumns := len(header)
for i := 1; i < len(records); i++ {
if len(records[i]) != numColumns {
return nil, fmt.Errorf("CSV file has invalid row at index %d", i)
}
}
return &CsvFile{
File: *file,
Header: header,
Rows: records[1:],
}, nil
}

View File

@@ -23,6 +23,8 @@ gist.header.download-zip: Download ZIP
gist.raw: Raw
gist.file-truncated: This file has been truncated.
gist.file-raw: This file can't be rendered.
gist.file-binary-edit: This file is binary.
gist.watch-full-file: View the full file.
gist.file-not-valid: This file is not a valid CSV file.
gist.no-content: No files found
@@ -115,6 +117,7 @@ gist.revision.file-renamed: renamed to
gist.revision.diff-truncated: Diff is too large to be shown
gist.revision.file-renamed-no-changes: File renamed without changes
gist.revision.empty-file: Empty file
gist.revision.binary-file-changes: Binary file changes are not shown
gist.revision.no-changes: No changes
gist.revision.no-revisions: No revisions to show
gist.revision-of: Revision of %s

44
internal/render/csv.go Normal file
View File

@@ -0,0 +1,44 @@
package render
import (
"encoding/csv"
"fmt"
"strings"
"github.com/thomiceli/opengist/internal/git"
)
type CSVFile struct {
*git.File
Type string `json:"type"`
Header []string `json:"-"`
Rows [][]string `json:"-"`
}
func (r CSVFile) getFile() *git.File {
return r.File
}
func renderCsvFile(file *git.File) (*CSVFile, error) {
reader := csv.NewReader(strings.NewReader(file.Content))
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
header := records[0]
numColumns := len(header)
for i := 1; i < len(records); i++ {
if len(records[i]) != numColumns {
return nil, fmt.Errorf("CSV file has invalid row at index %d", i)
}
}
return &CSVFile{
File: file,
Type: "CSV",
Header: header,
Rows: records[1:],
}, nil
}

View File

@@ -5,47 +5,44 @@ import (
"bytes"
"encoding/base64"
"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/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"path"
"sync"
)
type RenderedFile struct {
type HighlightedFile struct {
*git.File
Type string `json:"type"`
Lines []string `json:"-"`
HTML string `json:"-"`
}
func (r HighlightedFile) getFile() *git.File {
return r.File
}
type RenderedGist struct {
*db.Gist
Lines []string
HTML string
}
func HighlightFile(file *git.File) (RenderedFile, error) {
func highlightFile(file *git.File) (HighlightedFile, error) {
rendered := HighlightedFile{
File: file,
}
if !file.MimeType.IsText() {
return rendered, nil
}
style := newStyle()
lexer := newLexer(file.Filename)
if lexer.Config().Name == "markdown" {
return MarkdownFile(file)
}
if lexer.Config().Name == "XML" && path.Ext(file.Filename) == ".svg" {
return RenderSvgFile(file), nil
}
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
rendered := RenderedFile{
File: file,
}
iterator, err := lexer.Tokenise(nil, file.Content+"\n")
if err != nil {
return rendered, err
@@ -74,38 +71,6 @@ func HighlightFile(file *git.File) (RenderedFile, error) {
return rendered, err
}
func HighlightFiles(files []*git.File) []RenderedFile {
const numWorkers = 10
jobs := make(chan int, numWorkers)
renderedFiles := make([]RenderedFile, len(files))
var wg sync.WaitGroup
worker := func() {
for idx := range jobs {
rendered, err := HighlightFile(files[idx])
if err != nil {
log.Error().Err(err).Msg("Error rendering gist preview for " + files[idx].Filename)
}
renderedFiles[idx] = rendered
}
wg.Done()
}
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker()
}
for i := range files {
jobs <- i
}
close(jobs)
wg.Wait()
return renderedFiles
}
func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
rendered := RenderedGist{
Gist: gist,
@@ -146,18 +111,12 @@ func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
return rendered, err
}
func RenderSvgFile(file *git.File) RenderedFile {
rendered := RenderedFile{
func renderSvgFile(file *git.File) HighlightedFile {
return HighlightedFile{
File: file,
HTML: `<img src="data:image/svg+xml;base64,` + base64.StdEncoding.EncodeToString([]byte(file.Content)) + `" />`,
Type: "SVG",
}
encoded := base64.StdEncoding.EncodeToString([]byte(file.Content))
content := `<img src="data:image/svg+xml;base64,` + encoded + `" />`
rendered.HTML = content
rendered.Type = "SVG"
return rendered
}
func parseFileTypeName(config chroma.Config) string {

View File

@@ -2,6 +2,8 @@ package render
import (
"bytes"
"regexp"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
@@ -12,7 +14,6 @@ import (
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/util"
"go.abhg.dev/goldmark/mermaid"
"regexp"
)
func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) {
@@ -27,11 +28,11 @@ func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) {
}, err
}
func MarkdownFile(file *git.File) (RenderedFile, error) {
func renderMarkdownFile(file *git.File) (HighlightedFile, error) {
var buf bytes.Buffer
err := newMarkdownWithSvgExtension().Convert([]byte(file.Content), &buf)
return RenderedFile{
return HighlightedFile{
File: file,
HTML: buf.String(),
Type: "Markdown",

84
internal/render/render.go Normal file
View File

@@ -0,0 +1,84 @@
package render
import (
"path/filepath"
"sync"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git"
)
type RenderedFile interface {
getFile() *git.File
}
type NonHighlightedFile struct {
*git.File
Type string `json:"type"`
}
func (r NonHighlightedFile) getFile() *git.File {
return r.File
}
func RenderFiles(files []*git.File) []RenderedFile {
const numWorkers = 10
jobs := make(chan int, numWorkers)
renderedFiles := make([]RenderedFile, len(files))
var wg sync.WaitGroup
worker := func() {
for idx := range jobs {
renderedFiles[idx] = processFile(files[idx])
}
wg.Done()
}
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker()
}
for i := range files {
jobs <- i
}
close(jobs)
wg.Wait()
return renderedFiles
}
func processFile(file *git.File) RenderedFile {
mt := file.MimeType
if mt.IsCSV() {
rendered, err := renderCsvFile(file)
if err != nil {
log.Error().Err(err).Msg("Error parsing CSV file for " + file.Filename)
}
return rendered
} else if mt.IsText() && filepath.Ext(file.Filename) == ".md" {
rendered, err := renderMarkdownFile(file)
if err != nil {
log.Error().Err(err).Msg("Error rendering markdown file for " + file.Filename)
}
return rendered
} else if mt.IsSVG() {
rendered := renderSvgFile(file)
return rendered
} else if mt.CanBeEmbedded() {
rendered := NonHighlightedFile{File: file, Type: mt.RenderType()}
file.Content = ""
return rendered
} else if mt.CanBeRendered() {
rendered, err := highlightFile(file)
if err != nil {
log.Error().Err(err).Msg("Error rendering gist preview for " + file.Filename)
}
return rendered
} else {
rendered := NonHighlightedFile{File: file, Type: mt.RenderType()}
file.Content = ""
return rendered
}
}

View File

@@ -1,14 +1,15 @@
package gist
import (
"net/url"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/validator"
"github.com/thomiceli/opengist/internal/web/context"
"net/url"
"strconv"
"strings"
)
func Create(ctx *context.Context) error {

View File

@@ -7,7 +7,6 @@ import (
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"github.com/thomiceli/opengist/internal/web/handlers"
)
func RawFile(ctx *context.Context) error {
@@ -20,10 +19,8 @@ func RawFile(ctx *context.Context) error {
if file == nil {
return ctx.NotFound("File not found")
}
contentType := handlers.GetContentTypeFromFilename(file.Filename)
ContentDisposition := handlers.GetContentDisposition(file.Filename)
ctx.Response().Header().Set("Content-Type", contentType)
ctx.Response().Header().Set("Content-Disposition", ContentDisposition)
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
ctx.Response().Header().Set("Content-Disposition", "inline; filename=\""+file.Filename+"\"")
return ctx.PlainText(200, file.Content)
}
@@ -38,7 +35,7 @@ func DownloadFile(ctx *context.Context) error {
return ctx.NotFound("File not found")
}
ctx.Response().Header().Set("Content-Type", "text/plain")
ctx.Response().Header().Set("Content-Type", file.MimeType.ContentType)
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
_, err = ctx.Response().Write([]byte(file.Content))

View File

@@ -5,12 +5,13 @@ import (
"bytes"
gojson "encoding/json"
"fmt"
"net/url"
"time"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/render"
"github.com/thomiceli/opengist/internal/web/context"
"net/url"
"time"
)
func GistIndex(ctx *context.Context) error {
@@ -34,7 +35,7 @@ func GistIndex(ctx *context.Context) error {
return ctx.ErrorRes(500, "Error fetching files", err)
}
renderedFiles := render.HighlightFiles(files)
renderedFiles := render.RenderFiles(files)
ctx.SetData("page", "code")
ctx.SetData("commit", revision)
@@ -51,7 +52,7 @@ func GistJson(ctx *context.Context) error {
return ctx.ErrorRes(500, "Error fetching files", err)
}
renderedFiles := render.HighlightFiles(files)
renderedFiles := render.RenderFiles(files)
ctx.SetData("files", renderedFiles)
topics, err := gist.GetTopics()
@@ -106,7 +107,7 @@ func GistJs(ctx *context.Context) error {
return ctx.ErrorRes(500, "Error fetching files", err)
}
renderedFiles := render.HighlightFiles(files)
renderedFiles := render.RenderFiles(files)
ctx.SetData("files", renderedFiles)
htmlbuf := bytes.Buffer{}

View File

@@ -4,7 +4,6 @@ import (
"errors"
"html/template"
"net/url"
"path/filepath"
"strconv"
"strings"
@@ -141,22 +140,3 @@ func ParseSearchQueryStr(query string) (string, map[string]string) {
content := strings.TrimSpace(contentBuilder.String())
return content, metadata
}
func GetContentTypeFromFilename(filename string) (ret string) {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".css":
ret = "text/css"
default:
ret = "text/plain"
}
// add charset=utf-8, if not, unicode charset will be broken
ret += "; charset=utf-8"
return
}
func GetContentDisposition(filename string) string {
return "inline; filename=\"" + filename + "\""
}

View File

@@ -3,6 +3,13 @@ package server
import (
"errors"
"fmt"
"html/template"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@@ -15,12 +22,6 @@ import (
"github.com/thomiceli/opengist/internal/web/handlers"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"html/template"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"
)
func (s *Server) useCustomContext() {
@@ -54,7 +55,7 @@ func (s *Server) registerMiddlewares() {
return nil
},
}))
//s.echo.Use(middleware.Recover())
s.echo.Use(middleware.Recover())
s.echo.Use(middleware.Secure())
s.echo.Use(Middleware(sessionInit).toEcho())

View File

@@ -4,16 +4,6 @@ import (
gojson "encoding/json"
"errors"
"fmt"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/web/context"
"github.com/thomiceli/opengist/internal/web/handlers"
"github.com/thomiceli/opengist/public"
"github.com/thomiceli/opengist/templates"
htmlpkg "html"
"html/template"
"io"
@@ -24,6 +14,16 @@ import (
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/web/context"
"github.com/thomiceli/opengist/internal/web/handlers"
"github.com/thomiceli/opengist/public"
"github.com/thomiceli/opengist/templates"
)
type Template struct {
@@ -58,24 +58,6 @@ func (s *Server) setFuncMap() {
"isMarkdown": func(i string) bool {
return strings.ToLower(filepath.Ext(i)) == ".md"
},
"isCsv": func(i string) bool {
return strings.ToLower(filepath.Ext(i)) == ".csv"
},
"isSvg": func(i string) bool {
return strings.ToLower(filepath.Ext(i)) == ".svg"
},
"csvFile": func(file *git.File) *git.CsvFile {
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
return nil
}
csvFile, err := git.ParseCsv(file)
if err != nil {
return nil
}
return csvFile
},
"httpStatusText": http.StatusText,
"loadedTime": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"

9
package-lock.json generated
View File

@@ -5,6 +5,9 @@
"packages": {
"": {
"name": "opengist",
"dependencies": {
"pdfobject": "^2.3.1"
},
"devDependencies": {
"@codemirror/commands": "^6.2.2",
"@codemirror/lang-javascript": "^6.1.4",
@@ -3154,6 +3157,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pdfobject": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.3.1.tgz",
"integrity": "sha512-vluuGiSDmMGpOvWFGiUY4trNB8aGKLDVxIXuuGHjX0kK3bMxCANUVtLivctE7uejLBScWCnbVarKatFVvdwXaQ==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",

View File

@@ -25,6 +25,7 @@
"github-markdown-css": "^5.5.0",
"jdenticon": "^3.3.0",
"nodemon": "^2.0.22",
"pdfobject": "^2.3.1",
"postcss": "^8.4.32",
"postcss-cli": "^11.0.0",
"postcss-cssnext": "^3.1.1",

View File

@@ -196,8 +196,11 @@ document.addEventListener("DOMContentLoaded", () => {
let arr = Array.from(allEditorsdom);
arr.forEach((el: HTMLElement) => {
// in case we edit the gist contents
let currEditor = newEditor(el, el.querySelector<HTMLInputElement>(".form-filecontent")!.value);
editorsjs.push(currEditor);
let formFileContent =el.querySelector<HTMLInputElement>(".form-filecontent")
if (formFileContent !== null) {
let currEditor = newEditor(el, el.querySelector<HTMLInputElement>(".form-filecontent")!.value);
editorsjs.push(currEditor);
}
});
checkForFirstDeleteButton();

View File

@@ -1,3 +1,5 @@
import PDFObject from 'pdfobject';
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
el.addEventListener('click', event => {
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
@@ -75,5 +77,7 @@ if (document.getElementById('gist').dataset.own) {
});
}
document.querySelectorAll(".pdf").forEach((el) => {
PDFObject.embed(el.dataset.src || "", el);
})

4
public/style.css vendored
View File

@@ -189,3 +189,7 @@ dl.dl-config dd {
.hidden-important {
@apply hidden !important;
}
.pdfobject-container {
@apply min-h-[700px] h-[700px] !important;
}

View File

@@ -66,7 +66,7 @@
<div id="editors" class="space-y-4">
{{ if .dto.Files }}
{{ range .dto.Files }}
{{ template "_editor" dict "Filename" .Filename "Content" .Content "locale" $.locale }}
{{ template "_editor" dict "Filename" .Filename "Content" .Content "Binary" .Binary "locale" $.locale }}
{{ end }}
{{ else }}
{{ template "_editor" . }}

View File

@@ -3,7 +3,6 @@
{{ if .files }}
<div class="grid gap-y-4">
{{ range $file := .files }}
{{ $csv := csvFile $file.File }}
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto" data-file="{{ $file.Filename }}">
<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">
@@ -22,11 +21,13 @@
<a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/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">
{{ $.locale.Tr "gist.raw" }}
</a>
{{ if $file.MimeType.IsText }}
<button type="button" class="relative -ml-px inline-flex items-center bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700 copy-gist-btn">
<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="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-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 00-3.375-3.375H9.75" />
</svg>
</button>
{{ end }}
<a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/download/{{ $.commit }}/{{$file.Filename}}" class="relative -ml-px inline-flex items-center rounded-r-md bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700">
<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="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
@@ -36,29 +37,35 @@
<div class="hidden gist-content">{{ $file.Content }}</div>
</div>
{{ if $file.Truncated }}
{{ if and $file.Truncated $file.MimeType.IsText }}
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700">
{{ $.locale.Tr "gist.file-truncated" }} <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/{{ $.commit }}/{{$file.Filename}}">{{ $.locale.Tr "gist.watch-full-file" }}</a>
</div>
{{ end }}
{{ if and (not $csv) (isCsv $file.Filename) }}
{{ if not $file.MimeType.CanBeRendered }}
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700">
{{ $.locale.Tr "gist.file-raw" }} <a href="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/{{ $.commit }}/{{$file.Filename}}">{{ $.locale.Tr "gist.watch-full-file" }}</a>
</div>
{{ end }}
{{ if and (not $file.MimeType.IsText) ($file.MimeType.IsCSV) }}
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700">
{{ $.locale.Tr "gist.file-not-valid" }}
</div>
{{ end }}
</div>
<div class="overflow-auto">
{{ if $csv }}
{{ if $file.MimeType.IsText }}
{{ if $file.MimeType.IsCSV }}
<table class="csv-table">
<thead>
<tr>
{{ range $csv.Header }}
{{ range $file.Header }}
<th>{{ . }}</th>
{{ end }}
</tr>
</thead>
<tbody>
{{ range $csv.Rows }}
{{ range $file.Rows }}
<tr>
{{ range . }}
<td>{{ . }}</td>
@@ -68,7 +75,7 @@
</table>
{{ else if isMarkdown $file.Filename }}
<div class="chroma markdown markdown-body p-8">{{ $file.HTML | safe }}</div>
{{ else if isSvg $file.Filename }}
{{ else if $file.MimeType.IsSVG }}
<div class="p-8 flex justify-center">{{ $file.HTML | safe }}</div>
{{ else }}
<div class="code">
@@ -84,6 +91,25 @@
{{ end }}
</div>
{{ end }}
{{ else if $file.MimeType.IsImage }}
<div class="p-8 flex justify-center">
<img src="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/{{ $.commit }}/{{$file.Filename}}" alt="{{ $file.Filename }}" class="max-h-screen object-contain">
</div>
{{ else if $file.MimeType.IsAudio }}
<div class="p-8 flex justify-center">
<audio controls class="w-full max-w-lg">
<source src="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/{{ $.commit }}/{{$file.Filename}}" type="{{ $file.MimeType.ContentType }}">
</audio>
</div>
{{ else if $file.MimeType.IsVideo }}
<div class="p-8 flex justify-center">
<video controls class="w-full max-w-lg">
<source src="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/{{ $.commit }}/{{$file.Filename}}" type="{{ $file.MimeType.ContentType }}">
</video>
</div>
{{ else if $file.MimeType.IsPDF }}
<div class="pdf" data-src="{{ $.c.ExternalUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/{{ $.commit }}/{{$file.Filename}}"></div>
{{ end }}
</div>
</div>
{{ end }}

View File

@@ -6,23 +6,28 @@
<a target="_blank" href="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}#file-{{ slug $file.Filename }}"><span class="font-bold text-gray-700 dark:text-gray-200">{{ $file.Filename }}</span> &middot; {{ $file.HumanSize }} &middot; {{ $file.Type }}</a>
<span class="float-right"><a target="_blank" href="{{ $.baseHttpUrl }}">Hosted via Opengist</a> &middot; <span class="text-gray-700 dark:text-gray-200 font-bold"><a target="_blank" href="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}">view raw</a></span></span>
</div>
{{ if $file.Truncated }}
{{ if and $file.Truncated $file.MimeType.IsText }}
<div class="text-xs px-4 bg-gray-50 py-1.5 border-b-1 border-gray-100 dark:border-gray-700">
{{ $.locale.Tr "gist.file-truncated" }} <a target="_blank" class="text-primary-600" href="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}">{{ $.locale.Tr "gist.watch-full-file" }}</a>
</div>
{{ end }}
{{ $csv := csvFile $file.File }}
{{ if $csv }}
{{ if or (not $file.MimeType.CanBeRendered) $file.MimeType.IsPDF }}
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700">
{{ $.locale.Tr "gist.file-raw" }} <a href="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}">{{ $.locale.Tr "gist.watch-full-file" }}</a>
</div>
{{ end }}
{{ if $file.MimeType.IsText }}
{{ if $file.MimeType.IsCSV }}
<table class="csv-table">
<thead>
<tr>
{{ range $csv.Header }}
{{ range $file.Header }}
<th>{{ . }}</th>
{{ end }}
</tr>
</thead>
<tbody>
{{ range $csv.Rows }}
{{ range $file.Rows }}
<tr>
{{ range . }}
<td>{{ . }}</td>
@@ -32,7 +37,7 @@
</table>
{{ else if isMarkdown $file.Filename }}
<div class="chroma markdown markdown-body p-8">{{ $file.HTML | safe }}</div>
{{ else if isSvg $file.Filename }}
{{ else if $file.MimeType.IsSVG }}
<div class="p-8 flex justify-center">{{ $file.HTML | safe }}</div>
{{ else }}
<div class="code dark:bg-gray-900">
@@ -48,7 +53,25 @@
{{ end }}
</div>
{{ end }}
{{ else if $file.MimeType.IsImage }}
<div class="p-8 flex justify-center">
<img src="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}" alt="{{ $file.Filename }}" class="max-h-screen object-contain">
</div>
{{ else if $file.MimeType.IsAudio }}
<div class="p-8 flex justify-center">
<audio controls class="w-full max-w-lg">
<source src="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}" type="{{ $file.MimeType.ContentType }}">
</audio>
</div>
{{ else if $file.MimeType.IsVideo }}
<div class="p-8 flex justify-center">
<video controls class="w-full max-w-lg">
<source src="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}" type="{{ $file.MimeType.ContentType }}">
</video>
</div>
{{ else if $file.MimeType.IsPDF }}
<div class="pdf" data-src="{{ $.baseHttpUrl }}/{{ $.gist.User.Username }}/{{ $.gist.Identifier }}/raw/HEAD/{{$file.Filename}}"></div>
{{ end }}
</div>
{{ end }}
</div>

View File

@@ -49,6 +49,8 @@
<div class="overflow-auto">
{{ if $file.Truncated }}
<p class="m-2 ml-4 text-sm">{{ $.locale.Tr "gist.revision.diff-truncated" }}</p>
{{ else if $file.IsBinary }}
<p class="m-2 ml-4 text-sm">{{ $.locale.Tr "gist.revision.binary-file-changes" }}</p>
{{ else if and (eq $file.Content "") (ne $file.OldFilename "") }}
<p class="m-2 ml-4 text-sm">{{ $.locale.Tr "gist.revision.file-renamed-no-changes" }}</p>
{{ else if eq $file.Content "" }}

View File

@@ -3,13 +3,14 @@
<div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto flex">
<p class="mx-2 my-2 inline-flex">
<input type="text" name="name" value="{{ .Filename }}" placeholder="{{ $.locale.Tr "gist.new.filename-with-extension" }}" style="line-height: 0.05em" class="form-filename bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-l-md gist-title" autocomplete="off" data-lpignore data-bwignore data-1p-ignore>
<button style="line-height: 0.05em" class="delete-file -ml-px relative inline-flex items-center space-x-2 px-4 py-2 border border-gray-200 dark:border-gray-700 text-sm font-medium rounded-r-md text-slate-700 dark:text-slate-300 bg-gray-50 dark:bg-gray-800 hover:bg-white dark:hover:bg-gray-900 focus:outline-none" type="button">
<button style="line-height: 0.05em" class="delete-file -ml-px relative inline-flex items-center space-x-2 px-4 py-2 border text-rose-600 dark:text-rose-400 border-gray-200 dark:border-gray-700 text-sm font-medium rounded-r-md bg-gray-50 dark:bg-gray-800 hover:bg-rose-500 hover:text-white dark:hover:bg-rose-600 hover:border-rose-600 dark:hover:border-rose-700 dark:hover:text-white focus:outline-none" type="button">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</p>
<button type="button" class="md-preview hidden whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-900 my-2 px-2 text-xs font-medium shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">{{ $.locale.Tr "gist.new.preview" }}</button>
{{ if not .Binary }}
<div class="hidden mx-2 my-2 sm:inline-flex ml-auto space-x-2">
<select class="editor-indent-type whitespace-nowrap text-slate-700 dark:text-slate-300 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
<optgroup label="{{ $.locale.Tr "gist.new.indent-mode" }}">
@@ -31,8 +32,15 @@
</optgroup>
</select>
</div>
{{ end }}
</div>
<input type="hidden" value="{{ .Content }}" name="content" class="form-filecontent" autocomplete="off">
<div class="hidden preview chroma markdown markdown-body p-8"></div>
{{ if not .Binary }}
<input type="hidden" value="{{ .Content }}" name="content" class="form-filecontent" autocomplete="off">
<div class="hidden preview chroma markdown markdown-body p-8"></div>
{{ else }}
<div class="text-sm px-4 py-1.5 border-t-1 border-gray-200 dark:border-gray-700">
{{ $.locale.Tr "gist.file-binary-edit" }}
</div>
{{ end }}
</div>
{{ end }}