Allow different HardBreaks settings for documents and comments (#11515)

GH has different HardBreaks behaviour for markdown comments and documents.

Comments have hard breaks and documents have soft breaks - therefore Gitea's rendering will always be different from GH's if we only provide one setting.

Here we split the setting in to two - one for documents and one for comments and other things.

Signed-off-by: Andrew Thornton art27@cantab.net

Changes to index.js as per @silverwind 
Co-authored-by: silverwind <me@silverwind.io>

Changes to docs as per @guillep2k 
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
This commit is contained in:
zeripath 2020-05-24 09:14:26 +01:00 committed by GitHub
parent 3761bdb640
commit 814ca9ffea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 77 additions and 29 deletions

View file

@ -216,7 +216,10 @@ EVENT_SOURCE_UPDATE_TIME = 10s
; Render soft line breaks as hard line breaks, which means a single newline character between ; Render soft line breaks as hard line breaks, which means a single newline character between
; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not ; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not
; necessary to force a line break. ; necessary to force a line break.
ENABLE_HARD_LINE_BREAK = true ; Render soft line breaks as hard line breaks for comments
ENABLE_HARD_LINE_BREAK_IN_COMMENTS = true
; Render soft line breaks as hard line breaks for markdown documents
ENABLE_HARD_LINE_BREAK_IN_DOCUMENTS = false
; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown ; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown
; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes) ; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes)
; URLs starting with http and https are always displayed, whatever is put in this entry. ; URLs starting with http and https are always displayed, whatever is put in this entry.

View file

@ -152,7 +152,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
## Markdown (`markdown`) ## Markdown (`markdown`)
- `ENABLE_HARD_LINE_BREAK`: **true**: Render soft line breaks as hard line breaks, which - `ENABLE_HARD_LINE_BREAK_IN_COMMENTS`: **true**: Render soft line breaks as hard line breaks in comments, which
means a single newline character between paragraphs will cause a line break and adding
trailing whitespace to paragraphs is not necessary to force a line break.
- `ENABLE_HARD_LINE_BREAK_IN_DOCUMENTS`: **false**: Render soft line breaks as hard line breaks in documents, which
means a single newline character between paragraphs will cause a line break and adding means a single newline character between paragraphs will cause a line break and adding
trailing whitespace to paragraphs is not necessary to force a line break. trailing whitespace to paragraphs is not necessary to force a line break.
- `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional - `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional

View file

@ -175,6 +175,7 @@ type Repository struct {
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
RenderingMetas map[string]string `xorm:"-"` RenderingMetas map[string]string `xorm:"-"`
DocumentRenderingMetas map[string]string `xorm:"-"`
Units []*RepoUnit `xorm:"-"` Units []*RepoUnit `xorm:"-"`
PrimaryLanguage *LanguageStat `xorm:"-"` PrimaryLanguage *LanguageStat `xorm:"-"`
@ -545,11 +546,12 @@ func (repo *Repository) mustOwner(e Engine) *User {
// ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers. // ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers.
func (repo *Repository) ComposeMetas() map[string]string { func (repo *Repository) ComposeMetas() map[string]string {
if repo.RenderingMetas == nil { if len(repo.RenderingMetas) == 0 {
metas := map[string]string{ metas := map[string]string{
"user": repo.OwnerName, "user": repo.OwnerName,
"repo": repo.Name, "repo": repo.Name,
"repoPath": repo.RepoPath(), "repoPath": repo.RepoPath(),
"mode": "comment",
} }
unit, err := repo.GetUnit(UnitTypeExternalTracker) unit, err := repo.GetUnit(UnitTypeExternalTracker)
@ -581,6 +583,19 @@ func (repo *Repository) ComposeMetas() map[string]string {
return repo.RenderingMetas return repo.RenderingMetas
} }
// ComposeDocumentMetas composes a map of metas for properly rendering documents
func (repo *Repository) ComposeDocumentMetas() map[string]string {
if len(repo.DocumentRenderingMetas) == 0 {
metas := map[string]string{}
for k, v := range repo.ComposeMetas() {
metas[k] = v
}
metas["mode"] = "document"
repo.DocumentRenderingMetas = metas
}
return repo.DocumentRenderingMetas
}
// DeleteWiki removes the actual and local copy of repository wiki. // DeleteWiki removes the actual and local copy of repository wiki.
func (repo *Repository) DeleteWiki() error { func (repo *Repository) DeleteWiki() error {
return repo.deleteWiki(x) return repo.deleteWiki(x)

View file

@ -151,6 +151,16 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
v.AppendChild(v, newChild) v.AppendChild(v, newChild)
} }
} }
case *ast.Text:
if v.SoftLineBreak() && !v.HardLineBreak() {
renderMetas := pc.Get(renderMetasKey).(map[string]string)
mode := renderMetas["mode"]
if mode != "document" {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
} else {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
}
}
} }
return ast.WalkContinue, nil return ast.WalkContinue, nil
}) })

View file

@ -29,17 +29,19 @@ var once = sync.Once{}
var urlPrefixKey = parser.NewContextKey() var urlPrefixKey = parser.NewContextKey()
var isWikiKey = parser.NewContextKey() var isWikiKey = parser.NewContextKey()
var renderMetasKey = parser.NewContextKey()
// NewGiteaParseContext creates a parser.Context with the gitea context set // NewGiteaParseContext creates a parser.Context with the gitea context set
func NewGiteaParseContext(urlPrefix string, isWiki bool) parser.Context { func NewGiteaParseContext(urlPrefix string, metas map[string]string, isWiki bool) parser.Context {
pc := parser.NewContext(parser.WithIDs(newPrefixedIDs())) pc := parser.NewContext(parser.WithIDs(newPrefixedIDs()))
pc.Set(urlPrefixKey, urlPrefix) pc.Set(urlPrefixKey, urlPrefix)
pc.Set(isWikiKey, isWiki) pc.Set(isWikiKey, isWiki)
pc.Set(renderMetasKey, metas)
return pc return pc
} }
// RenderRaw renders Markdown to HTML without handling special links. // render renders Markdown to HTML without handling special links.
func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { func render(body []byte, urlPrefix string, metas map[string]string, wikiMarkdown bool) []byte {
once.Do(func() { once.Do(func() {
converter = goldmark.New( converter = goldmark.New(
goldmark.WithExtensions(extension.Table, goldmark.WithExtensions(extension.Table,
@ -75,12 +77,9 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
), ),
) )
if setting.Markdown.EnableHardLineBreak {
converter.Renderer().AddOptions(html.WithHardWraps())
}
}) })
pc := NewGiteaParseContext(urlPrefix, wikiMarkdown) pc := NewGiteaParseContext(urlPrefix, metas, wikiMarkdown)
var buf bytes.Buffer var buf bytes.Buffer
if err := converter.Convert(giteautil.NormalizeEOL(body), &buf, parser.WithContext(pc)); err != nil { if err := converter.Convert(giteautil.NormalizeEOL(body), &buf, parser.WithContext(pc)); err != nil {
log.Error("Unable to render: %v", err) log.Error("Unable to render: %v", err)
@ -112,7 +111,7 @@ func (Parser) Extensions() []string {
// Render implements markup.Parser // Render implements markup.Parser
func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
return RenderRaw(rawBytes, urlPrefix, isWiki) return render(rawBytes, urlPrefix, metas, isWiki)
} }
// Render renders Markdown to HTML with all specific handling stuff. // Render renders Markdown to HTML with all specific handling stuff.
@ -120,6 +119,11 @@ func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
return markup.Render("a.md", rawBytes, urlPrefix, metas) return markup.Render("a.md", rawBytes, urlPrefix, metas)
} }
// RenderRaw renders Markdown to HTML without handling special links.
func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
return render(body, urlPrefix, map[string]string{}, wikiMarkdown)
}
// RenderString renders Markdown to HTML with special links and returns string type. // RenderString renders Markdown to HTML with special links and returns string type.
func RenderString(raw, urlPrefix string, metas map[string]string) string { func RenderString(raw, urlPrefix string, metas map[string]string) string {
return markup.RenderString("a.md", raw, urlPrefix, metas) return markup.RenderString("a.md", raw, urlPrefix, metas)

View file

@ -256,11 +256,13 @@ var (
// Markdown settings // Markdown settings
Markdown = struct { Markdown = struct {
EnableHardLineBreak bool EnableHardLineBreakInComments bool
EnableHardLineBreakInDocuments bool
CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"`
FileExtensions []string FileExtensions []string
}{ }{
EnableHardLineBreak: true, EnableHardLineBreakInComments: true,
EnableHardLineBreakInDocuments: false,
FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","), FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","),
} }

View file

@ -48,10 +48,12 @@ func Markdown(ctx *context.APIContext, form api.MarkdownOption) {
} }
switch form.Mode { switch form.Mode {
case "comment":
fallthrough
case "gfm": case "gfm":
md := []byte(form.Text) md := []byte(form.Text)
urlPrefix := form.Context urlPrefix := form.Context
var meta map[string]string meta := map[string]string{}
if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) {
// check if urlPrefix is already set to a URL // check if urlPrefix is already set to a URL
linkRegex, _ := xurls.StrictMatchingScheme("https?://") linkRegex, _ := xurls.StrictMatchingScheme("https?://")
@ -61,8 +63,16 @@ func Markdown(ctx *context.APIContext, form api.MarkdownOption) {
} }
} }
if ctx.Repo != nil && ctx.Repo.Repository != nil { if ctx.Repo != nil && ctx.Repo.Repository != nil {
// "gfm" = Github Flavored Markdown - set this to render as a document
if form.Mode == "gfm" {
meta = ctx.Repo.Repository.ComposeDocumentMetas()
} else {
meta = ctx.Repo.Repository.ComposeMetas() meta = ctx.Repo.Repository.ComposeMetas()
} }
}
if form.Mode == "gfm" {
meta["mode"] = "document"
}
if form.Wiki { if form.Wiki {
_, err := ctx.Write([]byte(markdown.RenderWiki(md, urlPrefix, meta))) _, err := ctx.Write([]byte(markdown.RenderWiki(md, urlPrefix, meta)))
if err != nil { if err != nil {

View file

@ -94,7 +94,7 @@ Here are some links to the most important topics. You can find the full list of
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p> <p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
<h2 id="user-content-quick-links">Quick Links</h2> <h2 id="user-content-quick-links">Quick Links</h2>
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p> <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
<p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a><br/> <p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a>
<a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> <a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
`, `,
// Guard wiki sidebar: special syntax // Guard wiki sidebar: special syntax

View file

@ -319,7 +319,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
if markupType := markup.Type(readmeFile.name); markupType != "" { if markupType := markup.Type(readmeFile.name); markupType != "" {
ctx.Data["IsMarkup"] = true ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = string(markupType) ctx.Data["MarkupType"] = string(markupType)
ctx.Data["FileContent"] = string(markup.Render(readmeFile.name, buf, readmeTreelink, ctx.Repo.Repository.ComposeMetas())) ctx.Data["FileContent"] = string(markup.Render(readmeFile.name, buf, readmeTreelink, ctx.Repo.Repository.ComposeDocumentMetas()))
} else { } else {
ctx.Data["IsRenderedHTML"] = true ctx.Data["IsRenderedHTML"] = true
ctx.Data["FileContent"] = strings.Replace( ctx.Data["FileContent"] = strings.Replace(
@ -459,7 +459,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
if markupType := markup.Type(blob.Name()); markupType != "" { if markupType := markup.Type(blob.Name()); markupType != "" {
ctx.Data["IsMarkup"] = true ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType ctx.Data["MarkupType"] = markupType
ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeMetas())) ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeDocumentMetas()))
} else if readmeExist { } else if readmeExist {
ctx.Data["IsRenderedHTML"] = true ctx.Data["IsRenderedHTML"] = true
ctx.Data["FileContent"] = strings.Replace( ctx.Data["FileContent"] = strings.Replace(
@ -538,7 +538,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
buf = append(buf, d...) buf = append(buf, d...)
ctx.Data["IsMarkup"] = true ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType ctx.Data["MarkupType"] = markupType
ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeMetas())) ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeDocumentMetas()))
} }
} }

View file

@ -209,7 +209,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return nil, nil return nil, nil
} }
metas := ctx.Repo.Repository.ComposeMetas() metas := ctx.Repo.Repository.ComposeDocumentMetas()
ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas) ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas)
ctx.Data["sidebarPresent"] = sidebarContent != nil ctx.Data["sidebarPresent"] = sidebarContent != nil
ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas) ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas)

View file

@ -30,7 +30,7 @@
<div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff"> <div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff">
<a class="active item" data-tab="write">{{svg "octicon-code" 16}} {{if .IsNewFile}}{{.i18n.Tr "repo.editor.new_file"}}{{else}}{{.i18n.Tr "repo.editor.edit_file"}}{{end}}</a> <a class="active item" data-tab="write">{{svg "octicon-code" 16}} {{if .IsNewFile}}{{.i18n.Tr "repo.editor.new_file"}}{{else}}{{.i18n.Tr "repo.editor.edit_file"}}{{end}}</a>
{{if not .IsNewFile}} {{if not .IsNewFile}}
<a class="item" data-tab="preview" data-url="{{.Repository.APIURL}}/markdown" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL | EscapePound}}" data-preview-file-modes="{{.PreviewableFileModes}}">{{svg "octicon-eye" 16}} {{.i18n.Tr "preview"}}</a> <a class="item" data-tab="preview" data-url="{{.Repository.APIURL}}/markdown" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL | EscapePound}}" data-preview-file-modes="{{.PreviewableFileModes}}" data-markdown-mode="gfm">{{svg "octicon-eye" 16}} {{.i18n.Tr "preview"}}</a>
<a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName | EscapePound}}/{{.TreePath | EscapePound}}" data-context="{{.BranchLink}}">{{svg "octicon-diff" 16}} {{.i18n.Tr "repo.editor.preview_changes"}}</a> <a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName | EscapePound}}/{{.TreePath | EscapePound}}" data-context="{{.BranchLink}}">{{svg "octicon-diff" 16}} {{.i18n.Tr "repo.editor.preview_changes"}}</a>
{{end}} {{end}}
</div> </div>

View file

@ -41,7 +41,7 @@ function initCommentPreviewTab($form) {
const $this = $(this); const $this = $(this);
$.post($this.data('url'), { $.post($this.data('url'), {
_csrf: csrf, _csrf: csrf,
mode: 'gfm', mode: 'comment',
context: $this.data('context'), context: $this.data('context'),
text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val() text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val()
}, (data) => { }, (data) => {
@ -65,6 +65,7 @@ function initEditPreviewTab($form) {
$previewTab.on('click', function () { $previewTab.on('click', function () {
const $this = $(this); const $this = $(this);
let context = `${$this.data('context')}/`; let context = `${$this.data('context')}/`;
const mode = $this.data('markdown-mode') || 'comment';
const treePathEl = $form.find('input#tree_path'); const treePathEl = $form.find('input#tree_path');
if (treePathEl.length > 0) { if (treePathEl.length > 0) {
context += treePathEl.val(); context += treePathEl.val();
@ -72,7 +73,7 @@ function initEditPreviewTab($form) {
context = context.substring(0, context.lastIndexOf('/')); context = context.substring(0, context.lastIndexOf('/'));
$.post($this.data('url'), { $.post($this.data('url'), {
_csrf: csrf, _csrf: csrf,
mode: 'gfm', mode,
context, context,
text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val() text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val()
}, (data) => { }, (data) => {