github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/markup/orgmode/orgmode.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 package markup 7 8 import ( 9 "bytes" 10 "fmt" 11 "html" 12 "io" 13 "strings" 14 15 "github.com/gitbundle/modules/highlight" 16 "github.com/gitbundle/modules/log" 17 "github.com/gitbundle/modules/markup" 18 "github.com/gitbundle/modules/setting" 19 "github.com/gitbundle/modules/util" 20 21 "github.com/alecthomas/chroma" 22 "github.com/alecthomas/chroma/lexers" 23 "github.com/niklasfasching/go-org/org" 24 ) 25 26 func init() { 27 markup.RegisterRenderer(Renderer{}) 28 } 29 30 // Renderer implements markup.Renderer for orgmode 31 type Renderer struct{} 32 33 var _ markup.PostProcessRenderer = (*Renderer)(nil) 34 35 // Name implements markup.Renderer 36 func (Renderer) Name() string { 37 return "orgmode" 38 } 39 40 // NeedPostProcess implements markup.PostProcessRenderer 41 func (Renderer) NeedPostProcess() bool { return true } 42 43 // Extensions implements markup.Renderer 44 func (Renderer) Extensions() []string { 45 return []string{".org"} 46 } 47 48 // SanitizerRules implements markup.Renderer 49 func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { 50 return []setting.MarkupSanitizerRule{} 51 } 52 53 // Render renders orgmode rawbytes to HTML 54 func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { 55 htmlWriter := org.NewHTMLWriter() 56 htmlWriter.HighlightCodeBlock = func(source, lang string, inline bool) string { 57 defer func() { 58 if err := recover(); err != nil { 59 log.Error("Panic in HighlightCodeBlock: %v\n%s", err, log.Stack(2)) 60 panic(err) 61 } 62 }() 63 var w strings.Builder 64 if _, err := w.WriteString(`<pre>`); err != nil { 65 return "" 66 } 67 68 lexer := lexers.Get(lang) 69 if lexer == nil && lang == "" { 70 lexer = lexers.Analyse(source) 71 if lexer == nil { 72 lexer = lexers.Fallback 73 } 74 lang = strings.ToLower(lexer.Config().Name) 75 } 76 77 if lexer == nil { 78 // include language-x class as part of commonmark spec 79 if _, err := w.WriteString(`<code class="chroma language-` + string(lang) + `">`); err != nil { 80 return "" 81 } 82 if _, err := w.WriteString(html.EscapeString(source)); err != nil { 83 return "" 84 } 85 } else { 86 // include language-x class as part of commonmark spec 87 if _, err := w.WriteString(`<code class="chroma language-` + string(lang) + `">`); err != nil { 88 return "" 89 } 90 lexer = chroma.Coalesce(lexer) 91 92 if _, err := w.WriteString(highlight.CodeFromLexer(lexer, source)); err != nil { 93 return "" 94 } 95 } 96 97 if _, err := w.WriteString("</code></pre>"); err != nil { 98 return "" 99 } 100 101 return w.String() 102 } 103 104 w := &Writer{ 105 HTMLWriter: htmlWriter, 106 URLPrefix: ctx.URLPrefix, 107 IsWiki: ctx.IsWiki, 108 } 109 110 htmlWriter.ExtendingWriter = w 111 112 res, err := org.New().Silent().Parse(input, "").Write(w) 113 if err != nil { 114 return fmt.Errorf("orgmode.Render failed: %v", err) 115 } 116 _, err = io.Copy(output, strings.NewReader(res)) 117 return err 118 } 119 120 // RenderString renders orgmode string to HTML string 121 func RenderString(ctx *markup.RenderContext, content string) (string, error) { 122 var buf strings.Builder 123 if err := Render(ctx, strings.NewReader(content), &buf); err != nil { 124 return "", err 125 } 126 return buf.String(), nil 127 } 128 129 // Render renders orgmode string to HTML string 130 func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { 131 return Render(ctx, input, output) 132 } 133 134 // Writer implements org.Writer 135 type Writer struct { 136 *org.HTMLWriter 137 URLPrefix string 138 IsWiki bool 139 } 140 141 var byteMailto = []byte("mailto:") 142 143 // WriteRegularLink renders images, links or videos 144 func (r *Writer) WriteRegularLink(l org.RegularLink) { 145 link := []byte(html.EscapeString(l.URL)) 146 if l.Protocol == "file" { 147 link = link[len("file:"):] 148 } 149 if len(link) > 0 && !markup.IsLink(link) && 150 link[0] != '#' && !bytes.HasPrefix(link, byteMailto) { 151 lnk := string(link) 152 if r.IsWiki { 153 lnk = util.URLJoin("wiki", lnk) 154 } 155 link = []byte(util.URLJoin(r.URLPrefix, lnk)) 156 } 157 158 description := string(link) 159 if l.Description != nil { 160 description = r.WriteNodesAsString(l.Description...) 161 } 162 switch l.Kind() { 163 case "image": 164 imageSrc := getMediaURL(link) 165 fmt.Fprintf(r, `<img src="%s" alt="%s" title="%s" />`, imageSrc, description, description) 166 case "video": 167 videoSrc := getMediaURL(link) 168 fmt.Fprintf(r, `<video src="%s" title="%s">%s</video>`, videoSrc, description, description) 169 default: 170 fmt.Fprintf(r, `<a href="%s" title="%s">%s</a>`, link, description, description) 171 } 172 } 173 174 func getMediaURL(l []byte) string { 175 srcURL := string(l) 176 177 // Check if link is valid 178 if len(srcURL) > 0 && !markup.IsLink(l) { 179 srcURL = strings.Replace(srcURL, "/src/", "/media/", 1) 180 } 181 182 return srcURL 183 }