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