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  }