mirror of
https://github.com/thomiceli/opengist.git
synced 2024-12-22 12:32:40 +00:00
Add SVG parser (#346)
This commit is contained in:
parent
3068588111
commit
df226cbd99
11 changed files with 267 additions and 76 deletions
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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 := `<img src="data:image/svg+xml;base64,` + encoded + `" />`
|
||||
|
||||
rendered.HTML = content
|
||||
rendered.Type = "SVG"
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
func parseFileTypeName(config chroma.Config) string {
|
||||
fileType := config.Name
|
||||
if fileType == "fallback" || fileType == "plaintext" {
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
|
65
internal/render/markdown_checkbox.go
Normal file
65
internal/render/markdown_checkbox.go
Normal file
|
@ -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
|
||||
}
|
141
internal/render/markdown_svg.go
Normal file
141
internal/render/markdown_svg.go
Normal file
|
@ -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("<svg")) {
|
||||
return nil, parser.None
|
||||
}
|
||||
|
||||
if svgRegex.Match(line) {
|
||||
node = newSvgBlock()
|
||||
}
|
||||
|
||||
if node != nil {
|
||||
reader.Advance(segment.Len() - util.TrimRightSpaceLength(line))
|
||||
node.Lines().Append(segment)
|
||||
return node, parser.NoChildren
|
||||
}
|
||||
return nil, parser.None
|
||||
}
|
||||
|
||||
func (b *svgParser) Continue(node ast.Node, reader text.Reader, _ parser.Context) parser.State {
|
||||
line, segment := reader.PeekLine()
|
||||
if util.IsBlank(line) {
|
||||
return parser.Close
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(util.TrimRightSpace(line), []byte("</svg>")) {
|
||||
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 := `<img src="data:image/svg+xml;base64,` + encoded + `" />`
|
||||
_, _ = w.Write([]byte(imgTag))
|
||||
return ast.WalkContinue, nil
|
||||
}
|
|
@ -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
|
||||
|
|
4
public/embed.scss
vendored
4
public/embed.scss
vendored
|
@ -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;
|
||||
}
|
||||
|
|
4
public/style.css
vendored
4
public/style.css
vendored
|
@ -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;
|
||||
}
|
||||
|
|
2
templates/pages/gist.html
vendored
2
templates/pages/gist.html
vendored
|
@ -68,6 +68,8 @@
|
|||
</table>
|
||||
{{ else if isMarkdown $file.Filename }}
|
||||
<div class="chroma markdown markdown-body p-8">{{ $file.HTML | safe }}</div>
|
||||
{{ else if isSvg $file.Filename }}
|
||||
<div class="p-8 flex justify-center">{{ $file.HTML | safe }}</div>
|
||||
{{ else }}
|
||||
<div class="code">
|
||||
{{ $fileslug := slug $file.Filename }}
|
||||
|
|
2
templates/pages/gist_embed.html
vendored
2
templates/pages/gist_embed.html
vendored
|
@ -32,6 +32,8 @@
|
|||
</table>
|
||||
{{ else if isMarkdown $file.Filename }}
|
||||
<div class="chroma markdown markdown-body p-8">{{ $file.HTML | safe }}</div>
|
||||
{{ else if isSvg $file.Filename }}
|
||||
<div class="p-8 flex justify-center">{{ $file.HTML | safe }}</div>
|
||||
{{ else }}
|
||||
<div class="code dark:bg-gray-900">
|
||||
{{ $fileslug := slug $file.Filename }}
|
||||
|
|
Loading…
Reference in a new issue