mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-23 17:14:24 +00:00
58caf422e6
## 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:
79615f7428/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>
191 lines
5.7 KiB
Go
191 lines
5.7 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package setting
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
)
|
|
|
|
// ExternalMarkupRenderers represents the external markup renderers
|
|
var (
|
|
ExternalMarkupRenderers []*MarkupRenderer
|
|
ExternalSanitizerRules []MarkupSanitizerRule
|
|
MermaidMaxSourceCharacters int
|
|
)
|
|
|
|
const (
|
|
RenderContentModeSanitized = "sanitized"
|
|
RenderContentModeNoSanitizer = "no-sanitizer"
|
|
RenderContentModeIframe = "iframe"
|
|
)
|
|
|
|
// Markdown settings
|
|
var Markdown = struct {
|
|
EnableHardLineBreakInComments bool
|
|
EnableHardLineBreakInDocuments bool
|
|
CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"`
|
|
FileExtensions []string
|
|
EnableMath bool
|
|
}{
|
|
EnableHardLineBreakInComments: true,
|
|
EnableHardLineBreakInDocuments: false,
|
|
FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
|
|
EnableMath: true,
|
|
}
|
|
|
|
// MarkupRenderer defines the external parser configured in ini
|
|
type MarkupRenderer struct {
|
|
Enabled bool
|
|
MarkupName string
|
|
Command string
|
|
FileExtensions []string
|
|
IsInputFile bool
|
|
NeedPostProcess bool
|
|
MarkupSanitizerRules []MarkupSanitizerRule
|
|
RenderContentMode string
|
|
}
|
|
|
|
// MarkupSanitizerRule defines the policy for whitelisting attributes on
|
|
// certain elements.
|
|
type MarkupSanitizerRule struct {
|
|
Element string
|
|
AllowAttr string
|
|
Regexp *regexp.Regexp
|
|
AllowDataURIImages bool
|
|
}
|
|
|
|
func loadMarkupFrom(rootCfg ConfigProvider) {
|
|
mustMapSetting(rootCfg, "markdown", &Markdown)
|
|
|
|
MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000)
|
|
ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
|
|
ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)
|
|
|
|
for _, sec := range rootCfg.Section("markup").ChildSections() {
|
|
name := strings.TrimPrefix(sec.Name(), "markup.")
|
|
if name == "" {
|
|
log.Warn("name is empty, markup " + sec.Name() + "ignored")
|
|
continue
|
|
}
|
|
|
|
if name == "sanitizer" || strings.HasPrefix(name, "sanitizer.") {
|
|
newMarkupSanitizer(name, sec)
|
|
} else {
|
|
newMarkupRenderer(name, sec)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newMarkupSanitizer(name string, sec ConfigSection) {
|
|
rule, ok := createMarkupSanitizerRule(name, sec)
|
|
if ok {
|
|
if strings.HasPrefix(name, "sanitizer.") {
|
|
names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2)
|
|
name = names[0]
|
|
}
|
|
for _, renderer := range ExternalMarkupRenderers {
|
|
if name == renderer.MarkupName {
|
|
renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule)
|
|
return
|
|
}
|
|
}
|
|
ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
|
|
}
|
|
}
|
|
|
|
func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerRule, bool) {
|
|
var rule MarkupSanitizerRule
|
|
|
|
ok := false
|
|
if sec.HasKey("ALLOW_DATA_URI_IMAGES") {
|
|
rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false)
|
|
ok = true
|
|
}
|
|
|
|
if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") {
|
|
rule.Element = sec.Key("ELEMENT").Value()
|
|
rule.AllowAttr = sec.Key("ALLOW_ATTR").Value()
|
|
|
|
if rule.Element == "" || rule.AllowAttr == "" {
|
|
log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name)
|
|
return rule, false
|
|
}
|
|
|
|
regexpStr := sec.Key("REGEXP").Value()
|
|
if regexpStr != "" {
|
|
// Validate when parsing the config that this is a valid regular
|
|
// expression. Then we can use regexp.MustCompile(...) later.
|
|
compiled, err := regexp.Compile(regexpStr)
|
|
if err != nil {
|
|
log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
|
|
return rule, false
|
|
}
|
|
|
|
rule.Regexp = compiled
|
|
}
|
|
|
|
ok = true
|
|
}
|
|
|
|
if !ok {
|
|
log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name)
|
|
return rule, false
|
|
}
|
|
|
|
return rule, true
|
|
}
|
|
|
|
func newMarkupRenderer(name string, sec ConfigSection) {
|
|
extensionReg := regexp.MustCompile(`\.\w`)
|
|
|
|
extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
|
|
exts := make([]string, 0, len(extensions))
|
|
for _, extension := range extensions {
|
|
if !extensionReg.MatchString(extension) {
|
|
log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
|
|
} else {
|
|
exts = append(exts, extension)
|
|
}
|
|
}
|
|
|
|
if len(exts) == 0 {
|
|
log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
|
|
return
|
|
}
|
|
|
|
command := sec.Key("RENDER_COMMAND").MustString("")
|
|
if command == "" {
|
|
log.Warn(" RENDER_COMMAND is empty, markup " + name + " ignored")
|
|
return
|
|
}
|
|
|
|
if sec.HasKey("DISABLE_SANITIZER") {
|
|
log.Error("Deprecated setting `[markup.*]` `DISABLE_SANITIZER` present. This fallback will be removed in v1.18.0")
|
|
}
|
|
|
|
renderContentMode := sec.Key("RENDER_CONTENT_MODE").MustString(RenderContentModeSanitized)
|
|
if !sec.HasKey("RENDER_CONTENT_MODE") && sec.Key("DISABLE_SANITIZER").MustBool(false) {
|
|
renderContentMode = RenderContentModeNoSanitizer // if only the legacy DISABLE_SANITIZER exists, use it
|
|
}
|
|
if renderContentMode != RenderContentModeSanitized &&
|
|
renderContentMode != RenderContentModeNoSanitizer &&
|
|
renderContentMode != RenderContentModeIframe {
|
|
log.Error("invalid RENDER_CONTENT_MODE: %q, default to %q", renderContentMode, RenderContentModeSanitized)
|
|
renderContentMode = RenderContentModeSanitized
|
|
}
|
|
|
|
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
|
|
Enabled: sec.Key("ENABLED").MustBool(false),
|
|
MarkupName: name,
|
|
FileExtensions: exts,
|
|
Command: command,
|
|
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
|
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
|
|
RenderContentMode: renderContentMode,
|
|
})
|
|
}
|