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.

markup.go 5.7KB

Add .livemd as a markdown extension (#22730) ## Needs and benefits [Livebook](https://livebook.dev/) notebooks are used for code documentation and for deep dives and note-taking in the elixir ecosystem. Rendering these in these as Markdown on frogejo has many benefits, since livemd is a subset of markdown. Some of the benefits are: - New users of elixir and livebook are scared by unformated .livemd files, but are shown what they expect - Sharing a notebook is as easy as sharing a link, no need to install the software in order to see the results. [goldmark-meraid ](https://github.com/abhinav/goldmark-mermaid) is a mermaid-js parser already included in gitea. This makes the .livemd rendering integration feature complete. With this PR class diagrams, ER Diagrams, flow charts and much more will be rendered perfectly. With the additional functionality gitea will be an ideal tool for sharing resources with fellow software engineers working in the elixir ecosystem. Allowing the git forge to be used without needing to install any software. ## Feature Description This issue requests the .livemd extension to be added as a Markdown language extension. - `.livemd` is the extension of Livebook which is an Elixir version of Jupyter Notebook. - `.livemd` is` a subset of Markdown. This would require the .livemd to be recognized as a markdown file. The Goldmark the markdown parser should handle the parsing and rendering automatically. Here is the corresponding commit for GitHub linguist: https://github.com/github/linguist/pull/5672 Here is a sample page of a livemd file: https://github.com/github/linguist/blob/master/samples/Markdown/livebook.livemd ## Screenshots The first screenshot shows how github shows the sample .livemd in the browser. The second screenshot shows how mermaid js, renders my development notebook and its corresponding ER Diagram. The source code can be found here: https://codeberg.org/lgh/Termi/src/commit/79615f74281789a1f2967b57bad0c67c356cef1f/termiNotes.livemd ## Testing I just changed the file extension from `.livemd`to `.md`and the document already renders perfectly on codeberg. Check you can it out [here](https://codeberg.org/lgh/Termi/src/branch/livemd2md/termiNotes.md) --------- Co-authored-by: techknowlogick <techknowlogick@gitea.io>
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package setting
  4. import (
  5. "regexp"
  6. "strings"
  7. "code.gitea.io/gitea/modules/log"
  8. )
  9. // ExternalMarkupRenderers represents the external markup renderers
  10. var (
  11. ExternalMarkupRenderers []*MarkupRenderer
  12. ExternalSanitizerRules []MarkupSanitizerRule
  13. MermaidMaxSourceCharacters int
  14. )
  15. const (
  16. RenderContentModeSanitized = "sanitized"
  17. RenderContentModeNoSanitizer = "no-sanitizer"
  18. RenderContentModeIframe = "iframe"
  19. )
  20. // Markdown settings
  21. var Markdown = struct {
  22. EnableHardLineBreakInComments bool
  23. EnableHardLineBreakInDocuments bool
  24. CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"`
  25. FileExtensions []string
  26. EnableMath bool
  27. }{
  28. EnableHardLineBreakInComments: true,
  29. EnableHardLineBreakInDocuments: false,
  30. FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
  31. EnableMath: true,
  32. }
  33. // MarkupRenderer defines the external parser configured in ini
  34. type MarkupRenderer struct {
  35. Enabled bool
  36. MarkupName string
  37. Command string
  38. FileExtensions []string
  39. IsInputFile bool
  40. NeedPostProcess bool
  41. MarkupSanitizerRules []MarkupSanitizerRule
  42. RenderContentMode string
  43. }
  44. // MarkupSanitizerRule defines the policy for whitelisting attributes on
  45. // certain elements.
  46. type MarkupSanitizerRule struct {
  47. Element string
  48. AllowAttr string
  49. Regexp *regexp.Regexp
  50. AllowDataURIImages bool
  51. }
  52. func loadMarkupFrom(rootCfg ConfigProvider) {
  53. mustMapSetting(rootCfg, "markdown", &Markdown)
  54. MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000)
  55. ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
  56. ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)
  57. for _, sec := range rootCfg.Section("markup").ChildSections() {
  58. name := strings.TrimPrefix(sec.Name(), "markup.")
  59. if name == "" {
  60. log.Warn("name is empty, markup " + sec.Name() + "ignored")
  61. continue
  62. }
  63. if name == "sanitizer" || strings.HasPrefix(name, "sanitizer.") {
  64. newMarkupSanitizer(name, sec)
  65. } else {
  66. newMarkupRenderer(name, sec)
  67. }
  68. }
  69. }
  70. func newMarkupSanitizer(name string, sec ConfigSection) {
  71. rule, ok := createMarkupSanitizerRule(name, sec)
  72. if ok {
  73. if strings.HasPrefix(name, "sanitizer.") {
  74. names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2)
  75. name = names[0]
  76. }
  77. for _, renderer := range ExternalMarkupRenderers {
  78. if name == renderer.MarkupName {
  79. renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule)
  80. return
  81. }
  82. }
  83. ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
  84. }
  85. }
  86. func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerRule, bool) {
  87. var rule MarkupSanitizerRule
  88. ok := false
  89. if sec.HasKey("ALLOW_DATA_URI_IMAGES") {
  90. rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false)
  91. ok = true
  92. }
  93. if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") {
  94. rule.Element = sec.Key("ELEMENT").Value()
  95. rule.AllowAttr = sec.Key("ALLOW_ATTR").Value()
  96. if rule.Element == "" || rule.AllowAttr == "" {
  97. log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name)
  98. return rule, false
  99. }
  100. regexpStr := sec.Key("REGEXP").Value()
  101. if regexpStr != "" {
  102. // Validate when parsing the config that this is a valid regular
  103. // expression. Then we can use regexp.MustCompile(...) later.
  104. compiled, err := regexp.Compile(regexpStr)
  105. if err != nil {
  106. log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
  107. return rule, false
  108. }
  109. rule.Regexp = compiled
  110. }
  111. ok = true
  112. }
  113. if !ok {
  114. log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name)
  115. return rule, false
  116. }
  117. return rule, true
  118. }
  119. func newMarkupRenderer(name string, sec ConfigSection) {
  120. extensionReg := regexp.MustCompile(`\.\w`)
  121. extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
  122. exts := make([]string, 0, len(extensions))
  123. for _, extension := range extensions {
  124. if !extensionReg.MatchString(extension) {
  125. log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
  126. } else {
  127. exts = append(exts, extension)
  128. }
  129. }
  130. if len(exts) == 0 {
  131. log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
  132. return
  133. }
  134. command := sec.Key("RENDER_COMMAND").MustString("")
  135. if command == "" {
  136. log.Warn(" RENDER_COMMAND is empty, markup " + name + " ignored")
  137. return
  138. }
  139. if sec.HasKey("DISABLE_SANITIZER") {
  140. log.Error("Deprecated setting `[markup.*]` `DISABLE_SANITIZER` present. This fallback will be removed in v1.18.0")
  141. }
  142. renderContentMode := sec.Key("RENDER_CONTENT_MODE").MustString(RenderContentModeSanitized)
  143. if !sec.HasKey("RENDER_CONTENT_MODE") && sec.Key("DISABLE_SANITIZER").MustBool(false) {
  144. renderContentMode = RenderContentModeNoSanitizer // if only the legacy DISABLE_SANITIZER exists, use it
  145. }
  146. if renderContentMode != RenderContentModeSanitized &&
  147. renderContentMode != RenderContentModeNoSanitizer &&
  148. renderContentMode != RenderContentModeIframe {
  149. log.Error("invalid RENDER_CONTENT_MODE: %q, default to %q", renderContentMode, RenderContentModeSanitized)
  150. renderContentMode = RenderContentModeSanitized
  151. }
  152. ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
  153. Enabled: sec.Key("ENABLED").MustBool(false),
  154. MarkupName: name,
  155. FileExtensions: exts,
  156. Command: command,
  157. IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
  158. NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
  159. RenderContentMode: renderContentMode,
  160. })
  161. }