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