You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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