diff --git a/go.mod b/go.mod index 40ce77d..e4505ab 100644 --- a/go.mod +++ b/go.mod @@ -97,7 +97,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.etcd.io/bbolt v1.3.10 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.23.0 // indirect diff --git a/go.sum b/go.sum index b8c707a..32236c8 100644 --- a/go.sum +++ b/go.sum @@ -232,8 +232,8 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= diff --git a/internal/render/highlight.go b/internal/render/highlight.go index 2098c2c..18a4bb1 100644 --- a/internal/render/highlight.go +++ b/internal/render/highlight.go @@ -3,6 +3,7 @@ package render import ( "bufio" "bytes" + "encoding/base64" "fmt" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters/html" @@ -11,6 +12,7 @@ import ( "github.com/rs/zerolog/log" "github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/git" + "path" "sync" ) @@ -28,18 +30,22 @@ type RenderedGist struct { } 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) } + 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 @@ -140,6 +146,20 @@ func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) { return rendered, err } +func RenderSvgFile(file *git.File) RenderedFile { + rendered := RenderedFile{ + File: file, + } + + encoded := base64.StdEncoding.EncodeToString([]byte(file.Content)) + content := `` + + rendered.HTML = content + rendered.Type = "SVG" + + return rendered +} + func parseFileTypeName(config chroma.Config) string { fileType := config.Name if fileType == "fallback" || fileType == "plaintext" { diff --git a/internal/render/markdown.go b/internal/render/markdown.go index 8e821ac..b9efd2e 100644 --- a/internal/render/markdown.go +++ b/internal/render/markdown.go @@ -1,24 +1,17 @@ package render import ( - "bufio" "bytes" - "github.com/Kunde21/markdownfmt/v3" "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/rs/zerolog/log" "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/ast" "github.com/yuin/goldmark/extension" - astex "github.com/yuin/goldmark/extension/ast" "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" "go.abhg.dev/goldmark/mermaid" - "strconv" ) func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) { @@ -33,7 +26,7 @@ func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) { func MarkdownFile(file *git.File) (RenderedFile, error) { var buf bytes.Buffer - err := newMarkdown().Convert([]byte(file.Content), &buf) + err := newMarkdownWithSvgExtension().Convert([]byte(file.Content), &buf) return RenderedFile{ File: file, @@ -43,77 +36,34 @@ func MarkdownFile(file *git.File) (RenderedFile, error) { } func MarkdownString(content string) (string, error) { var buf bytes.Buffer - err := newMarkdown().Convert([]byte(content), &buf) + err := newMarkdownWithSvgExtension().Convert([]byte(content), &buf) return buf.String(), 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{}, +func newMarkdown(extraExtensions ...goldmark.Extender) goldmark.Markdown { + extensions := []goldmark.Extender{ + extension.GFM, + highlighting.NewHighlighting( + highlighting.WithStyle("catppuccin-latte"), + highlighting.WithFormatOptions(html.WithClasses(true)), ), + emoji.Emoji, + &mermaid.Extender{}, + } + + extensions = append(extensions, extraExtensions...) + + return goldmark.New( + goldmark.WithExtensions(extensions...), goldmark.WithParserOptions( parser.WithASTTransformers( - util.Prioritized(&CheckboxTransformer{}, 10000), + util.Prioritized(&checkboxTransformer{}, 10000), ), ), ) } -type CheckboxTransformer struct{} - -func (t *CheckboxTransformer) Transform(node *ast.Document, _ text.Reader, _ parser.Context) { - i := 1 - err := ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - if entering { - if _, ok := n.(*astex.TaskCheckBox); ok { - listitem := n.Parent().Parent() - listitem.SetAttribute([]byte("data-checkbox-nb"), []byte(strconv.Itoa(i))) - i += 1 - } - } - return ast.WalkContinue, nil - }) - if err != nil { - log.Err(err) - } -} - -func Checkbox(content string, checkboxNb int) (string, error) { - buf := bytes.Buffer{} - w := bufio.NewWriter(&buf) - - source := []byte(content) - markdown := markdownfmt.NewGoldmark() - reader := text.NewReader(source) - document := markdown.Parser().Parse(reader) - - i := 1 - err := ast.Walk(document, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - if entering { - if listItem, ok := n.(*astex.TaskCheckBox); ok { - if i == checkboxNb { - listItem.IsChecked = !listItem.IsChecked - } - i += 1 - } - } - return ast.WalkContinue, nil - }) - if err != nil { - return "", err - } - - if err = markdown.Renderer().Render(w, source, document); err != nil { - return "", err - } - _ = w.Flush() - - return buf.String(), nil +func newMarkdownWithSvgExtension() goldmark.Markdown { + return newMarkdown(&svgToImgBase64{}) } diff --git a/internal/render/markdown_checkbox.go b/internal/render/markdown_checkbox.go new file mode 100644 index 0000000..94e64dd --- /dev/null +++ b/internal/render/markdown_checkbox.go @@ -0,0 +1,65 @@ +package render + +import ( + "bufio" + "bytes" + "github.com/Kunde21/markdownfmt/v3" + "github.com/rs/zerolog/log" + "github.com/yuin/goldmark/ast" + astex "github.com/yuin/goldmark/extension/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "strconv" +) + +type checkboxTransformer struct{} + +func (t *checkboxTransformer) Transform(node *ast.Document, _ text.Reader, _ parser.Context) { + i := 1 + err := ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + if _, ok := n.(*astex.TaskCheckBox); ok { + listitem := n.Parent().Parent() + listitem.SetAttribute([]byte("data-checkbox-nb"), []byte(strconv.Itoa(i))) + i += 1 + } + } + return ast.WalkContinue, nil + }) + if err != nil { + log.Err(err) + } +} + +func Checkbox(content string, checkboxNb int) (string, error) { + buf := bytes.Buffer{} + w := bufio.NewWriter(&buf) + + source := []byte(content) + markdown := markdownfmt.NewGoldmark() + reader := text.NewReader(source) + document := markdown.Parser().Parse(reader) + + i := 1 + err := ast.Walk(document, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + if listItem, ok := n.(*astex.TaskCheckBox); ok { + if i == checkboxNb { + listItem.IsChecked = !listItem.IsChecked + } + i += 1 + } + } + return ast.WalkContinue, nil + }) + if err != nil { + return "", err + } + + if err = markdown.Renderer().Render(w, source, document); err != nil { + return "", err + } + _ = w.Flush() + + return buf.String(), nil +} diff --git a/internal/render/markdown_svg.go b/internal/render/markdown_svg.go new file mode 100644 index 0000000..521de14 --- /dev/null +++ b/internal/render/markdown_svg.go @@ -0,0 +1,141 @@ +package render + +import ( + "bytes" + "encoding/base64" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + "regexp" +) + +var svgRegex = regexp.MustCompile(`(?i)^[ ]{0,3}<(svg)(?:\s.*|>.*|/>.*|)(?:\r\n|\n)?$`) + +type svgToImgBase64 struct{} + +func (e *svgToImgBase64) Extend(m goldmark.Markdown) { + m.Parser().AddOptions(parser.WithBlockParsers( + util.Prioritized(newSvgParser(), 1), + )) + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(newSvgRenderer(), 1), + )) +} + +// -- SVG Block -- // + +type svgBlock struct { + ast.BaseBlock +} + +func (n *svgBlock) IsRaw() bool { + return true +} + +func (n *svgBlock) Dump(source []byte, level int) { + ast.DumpHelper(n, source, level, nil, nil) +} + +var svgBlockKind = ast.NewNodeKind("SVG") + +func (n *svgBlock) Kind() ast.NodeKind { + return svgBlockKind +} + +func newSvgBlock() *svgBlock { + return &svgBlock{ + BaseBlock: ast.BaseBlock{}, + } +} + +// -- SVG Parser -- // + +type svgParser struct { +} + +var defaultSvgParser = &svgParser{} + +func newSvgParser() parser.BlockParser { + return defaultSvgParser +} + +func (b *svgParser) Trigger() []byte { + return []byte{'<'} +} + +func (b *svgParser) Open(parent ast.Node, reader text.Reader, _ parser.Context) (ast.Node, parser.State) { + var node *svgBlock + line, segment := reader.PeekLine() + + if !bytes.HasPrefix(line, []byte("")) { + node.Lines().Append(segment) + return parser.Continue | parser.NoChildren + } + + node.Lines().Append(segment) + reader.Advance(segment.Len()) + return parser.Close +} + +func (b *svgParser) Close(_ ast.Node, _ text.Reader, _ parser.Context) {} + +func (b *svgParser) CanInterruptParagraph() bool { + return true +} + +func (b *svgParser) CanAcceptIndentedLine() bool { + return false +} + +// -- SVG Renderer -- // + +type svgRenderer struct{} + +func newSvgRenderer() renderer.NodeRenderer { + return &svgRenderer{} +} + +func (r *svgRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(svgBlockKind, r.renderSVG) +} + +func (r *svgRenderer) renderSVG(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + rawHTML := node.(*svgBlock) + var svgContent []byte + for i := 0; i < rawHTML.Lines().Len(); i++ { + segment := rawHTML.Lines().At(i) + svgContent = append(svgContent, segment.Value(source)...) + } + encoded := base64.StdEncoding.EncodeToString(svgContent) + imgTag := `` + _, _ = w.Write([]byte(imgTag)) + return ast.WalkContinue, nil +} diff --git a/internal/web/server.go b/internal/web/server.go index 65abe0a..cbbc485 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -65,6 +65,9 @@ var ( "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 diff --git a/public/embed.scss b/public/embed.scss index 5ac1941..63effbb 100644 --- a/public/embed.scss +++ b/public/embed.scss @@ -107,6 +107,10 @@ dl.dl-config dd { @apply overflow-auto whitespace-pre; } +.markdown-body img { + @apply bg-transparent dark:bg-transparent; +} + .chroma.preview.markdown pre code { @apply p-4; } diff --git a/public/style.css b/public/style.css index 85422f2..4659e5c 100644 --- a/public/style.css +++ b/public/style.css @@ -167,6 +167,10 @@ dl.dl-config dd { @apply overflow-auto whitespace-pre !important; } +.markdown-body img { + @apply bg-transparent dark:bg-transparent !important; +} + .chroma.preview.markdown pre code { @apply p-4 !important; } diff --git a/templates/pages/gist.html b/templates/pages/gist.html index 4095b0c..26cbe8f 100644 --- a/templates/pages/gist.html +++ b/templates/pages/gist.html @@ -68,6 +68,8 @@ {{ else if isMarkdown $file.Filename }}
{{ $file.HTML | safe }}
+ {{ else if isSvg $file.Filename }} +
{{ $file.HTML | safe }}
{{ else }}
{{ $fileslug := slug $file.Filename }} diff --git a/templates/pages/gist_embed.html b/templates/pages/gist_embed.html index e051c76..6515977 100644 --- a/templates/pages/gist_embed.html +++ b/templates/pages/gist_embed.html @@ -32,6 +32,8 @@ {{ else if isMarkdown $file.Filename }}
{{ $file.HTML | safe }}
+ {{ else if isSvg $file.Filename }} +
{{ $file.HTML | safe }}
{{ else }}
{{ $fileslug := slug $file.Filename }}