mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-31 03:14:17 +09:00
enhance(markup): align markdown rendering details with GitHub (#39955)
Fixes markdown rendering details where Gitea deviated from GitHub unintentionally, and adds a `/devtest/markdown` page to review markdown rendering. - Paragraphs get the intended 1.5 line height - Blockquotes, alerts and definition lists no longer inherit browser default margins - Loose task lists keep their spacing, the checkbox renders inside the paragraph like on GitHub - Footnote references render as `[1]` so adjacent ones stay apart without CSS - Code in headings, headings in summaries, nested ordered lists, tables, `<mark>`, alert icons and `<hr>` follow GitHub Co-authored-by: bircni <bircni@icloud.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
13 files changed
+401
-169
No files matched your search
@@ -401,8 +401,10 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt
|
||||
_, _ = w.WriteString(`"><a href="#fn:user-content-`)
|
||||
_, _ = w.Write(n.Name)
|
||||
_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) // FIXME: here and below, need to keep the classes
|
||||
// literal brackets separate adjacent refs, also where no CSS applies like in mails
|
||||
_, _ = w.WriteString("[")
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(` </a></sup>`) // the style doesn't work at the moment, so add a space to separate the names
|
||||
_, _ = w.WriteString("]</a></sup>")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestRender_IssueList(t *testing.T) {
|
||||
</ul>`},
|
||||
{
|
||||
"IssueFootnote", "foo[^1][^2]\n\n[^1]: bar\n[^2]: baz",
|
||||
`<p>foo<sup id="fnref:user-content-1-12345"><a href="#fn:user-content-1-12345" rel="nofollow">1 </a></sup><sup id="fnref:user-content-2-12345"><a href="#fn:user-content-2-12345" rel="nofollow">2 </a></sup></p>
|
||||
`<p>foo<sup id="fnref:user-content-1-12345"><a href="#fn:user-content-1-12345" rel="nofollow">[1]</a></sup><sup id="fnref:user-content-2-12345"><a href="#fn:user-content-2-12345" rel="nofollow">[2]</a></sup></p>
|
||||
<div>
|
||||
<hr/>
|
||||
<ol>
|
||||
|
||||
@@ -5,7 +5,6 @@ package markdown
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
@@ -56,36 +55,6 @@ func NewSummary() *Summary {
|
||||
return &Summary{}
|
||||
}
|
||||
|
||||
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
|
||||
type TaskCheckBoxListItem struct {
|
||||
*ast.ListItem
|
||||
IsChecked bool
|
||||
SourcePosition int
|
||||
}
|
||||
|
||||
// KindTaskCheckBoxListItem is the NodeKind for TaskCheckBoxListItem
|
||||
var KindTaskCheckBoxListItem = ast.NewNodeKind("TaskCheckBoxListItem")
|
||||
|
||||
// Dump implements Node.Dump .
|
||||
func (n *TaskCheckBoxListItem) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["IsChecked"] = strconv.FormatBool(n.IsChecked)
|
||||
m["SourcePosition"] = strconv.FormatInt(int64(n.SourcePosition), 10)
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *TaskCheckBoxListItem) Kind() ast.NodeKind {
|
||||
return KindTaskCheckBoxListItem
|
||||
}
|
||||
|
||||
// NewTaskCheckBoxListItem returns a new TaskCheckBoxListItem node.
|
||||
func NewTaskCheckBoxListItem(listItem *ast.ListItem) *TaskCheckBoxListItem {
|
||||
return &TaskCheckBoxListItem{
|
||||
ListItem: listItem,
|
||||
}
|
||||
}
|
||||
|
||||
// Icon is an inline for a Fomantic UI icon
|
||||
type Icon struct {
|
||||
ast.BaseInline
|
||||
|
||||
@@ -146,7 +146,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
|
||||
reg.Register(KindAttention, r.renderAttention)
|
||||
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
|
||||
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
reg.Register(KindRawHTML, r.renderRawHTML)
|
||||
}
|
||||
|
||||
@@ -125,6 +125,10 @@ Here are some links to the most important topics. You can find the full list of
|
||||
- [x] checked
|
||||
- [ ] still unchecked
|
||||
|
||||
* [x] loose
|
||||
|
||||
second paragraph
|
||||
|
||||
### Definition list
|
||||
|
||||
First Term
|
||||
@@ -209,6 +213,12 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="450" checked=""/>checked</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="464"/>still unchecked</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="task-list-item">
|
||||
<p><input type="checkbox" disabled="" data-source-position="487" checked=""/>loose</p>
|
||||
<p>second paragraph</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 id="user-content-definition-list">Definition list</h3>
|
||||
<dl>
|
||||
<dt>First Term</dt>
|
||||
@@ -218,7 +228,7 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
|
||||
<dd>This is another definition of the second term.</dd>
|
||||
</dl>
|
||||
<h3 id="user-content-footnotes">Footnotes</h3>
|
||||
<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1 </a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2 </a></sup></p>
|
||||
<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">[1]</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">[2]</a></sup></p>
|
||||
<div>
|
||||
<hr/>
|
||||
<ol>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/modules/markup"
|
||||
|
||||
@@ -14,71 +14,37 @@ import (
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*TaskCheckBoxListItem) //nolint:forcetypeassert // registered for KindTaskCheckBoxListItem only
|
||||
if entering {
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<li")
|
||||
html.RenderAttributes(w, n, html.ListItemAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<li>")
|
||||
}
|
||||
fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
|
||||
if n.IsChecked {
|
||||
_, _ = w.WriteString(` checked=""`)
|
||||
}
|
||||
if r.XHTML {
|
||||
_, _ = w.WriteString(` />`)
|
||||
} else {
|
||||
_ = w.WriteByte('>')
|
||||
}
|
||||
fc := n.FirstChild()
|
||||
if fc != nil {
|
||||
if _, ok := fc.(*ast.TextBlock); !ok {
|
||||
_ = w.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
checkBox, ok := node.(*east.TaskCheckBox)
|
||||
if !entering || !ok {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
_, _ = w.WriteString(`<input type="checkbox" disabled=""`)
|
||||
html.RenderAttributes(w, checkBox, nil)
|
||||
if checkBox.IsChecked {
|
||||
_, _ = w.WriteString(` checked=""`)
|
||||
}
|
||||
if r.XHTML {
|
||||
_, _ = w.WriteString(` />`)
|
||||
} else {
|
||||
_, _ = w.WriteString("</li>\n")
|
||||
_ = w.WriteByte('>')
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) transformList(_ *markup.RenderContext, v *ast.List, rc *RenderConfig) {
|
||||
if v.HasChildren() {
|
||||
children := make([]ast.Node, 0, v.ChildCount())
|
||||
child := v.FirstChild()
|
||||
for child != nil {
|
||||
children = append(children, child)
|
||||
child = child.NextSibling()
|
||||
for listItem := v.FirstChild(); listItem != nil; listItem = listItem.NextSibling() {
|
||||
block := listItem.FirstChild()
|
||||
if block == nil {
|
||||
continue
|
||||
}
|
||||
v.RemoveChildren(v)
|
||||
|
||||
for _, child := range children {
|
||||
listItem := child.(*ast.ListItem) //nolint:forcetypeassert // a list only holds list items
|
||||
if !child.HasChildren() || !child.FirstChild().HasChildren() {
|
||||
v.AppendChild(v, child)
|
||||
continue
|
||||
}
|
||||
taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
|
||||
if !ok {
|
||||
v.AppendChild(v, child)
|
||||
continue
|
||||
}
|
||||
newChild := NewTaskCheckBoxListItem(listItem)
|
||||
newChild.IsChecked = taskCheckBox.IsChecked
|
||||
newChild.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("task-list-item")))
|
||||
segments := newChild.FirstChild().Lines()
|
||||
if segments.Len() > 0 {
|
||||
segment := segments.At(0)
|
||||
newChild.SourcePosition = rc.metaLength + segment.Start
|
||||
}
|
||||
v.AppendChild(v, newChild)
|
||||
checkBox, ok := block.FirstChild().(*east.TaskCheckBox)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
listItem.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("task-list-item")))
|
||||
if lines := block.Lines(); lines.Len() > 0 {
|
||||
checkBox.SetAttributeString("data-source-position", []byte(strconv.Itoa(rc.metaLength+lines.At(0).Start)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user