Refactor markdown render (#30139)
Only split the file into small ones (and rename AttentionTypes to attentionTypes)
This commit is contained in:
parent
7fda109aba
commit
71706126b5
|
@ -4,19 +4,14 @@
|
||||||
package markdown
|
package markdown
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/markup/common"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/svg"
|
|
||||||
giteautil "code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday/css"
|
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
east "github.com/yuin/goldmark/extension/ast"
|
east "github.com/yuin/goldmark/extension/ast"
|
||||||
"github.com/yuin/goldmark/parser"
|
"github.com/yuin/goldmark/parser"
|
||||||
|
@ -28,12 +23,12 @@ import (
|
||||||
|
|
||||||
// ASTTransformer is a default transformer of the goldmark tree.
|
// ASTTransformer is a default transformer of the goldmark tree.
|
||||||
type ASTTransformer struct {
|
type ASTTransformer struct {
|
||||||
AttentionTypes container.Set[string]
|
attentionTypes container.Set[string]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewASTTransformer() *ASTTransformer {
|
func NewASTTransformer() *ASTTransformer {
|
||||||
return &ASTTransformer{
|
return &ASTTransformer{
|
||||||
AttentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
|
attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,123 +61,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||||
|
|
||||||
switch v := n.(type) {
|
switch v := n.(type) {
|
||||||
case *ast.Heading:
|
case *ast.Heading:
|
||||||
for _, attr := range v.Attributes() {
|
g.transformHeading(ctx, v, reader, &tocList)
|
||||||
if _, ok := attr.Value.([]byte); !ok {
|
|
||||||
v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
txt := n.Text(reader.Source())
|
|
||||||
header := markup.Header{
|
|
||||||
Text: util.BytesToReadOnlyString(txt),
|
|
||||||
Level: v.Level,
|
|
||||||
}
|
|
||||||
if id, found := v.AttributeString("id"); found {
|
|
||||||
header.ID = util.BytesToReadOnlyString(id.([]byte))
|
|
||||||
}
|
|
||||||
tocList = append(tocList, header)
|
|
||||||
g.applyElementDir(v)
|
|
||||||
case *ast.Paragraph:
|
case *ast.Paragraph:
|
||||||
g.applyElementDir(v)
|
g.applyElementDir(v)
|
||||||
case *ast.Image:
|
case *ast.Image:
|
||||||
// Images need two things:
|
g.transformImage(ctx, v, reader)
|
||||||
//
|
|
||||||
// 1. Their src needs to munged to be a real value
|
|
||||||
// 2. If they're not wrapped with a link they need a link wrapper
|
|
||||||
|
|
||||||
// Check if the destination is a real link
|
|
||||||
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
|
|
||||||
v.Destination = []byte(giteautil.URLJoin(
|
|
||||||
ctx.Links.ResolveMediaLink(ctx.IsWiki),
|
|
||||||
strings.TrimLeft(string(v.Destination), "/"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
parent := n.Parent()
|
|
||||||
// Create a link around image only if parent is not already a link
|
|
||||||
if _, ok := parent.(*ast.Link); !ok && parent != nil {
|
|
||||||
next := n.NextSibling()
|
|
||||||
|
|
||||||
// Create a link wrapper
|
|
||||||
wrap := ast.NewLink()
|
|
||||||
wrap.Destination = v.Destination
|
|
||||||
wrap.Title = v.Title
|
|
||||||
wrap.SetAttributeString("target", []byte("_blank"))
|
|
||||||
|
|
||||||
// Duplicate the current image node
|
|
||||||
image := ast.NewImage(ast.NewLink())
|
|
||||||
image.Destination = v.Destination
|
|
||||||
image.Title = v.Title
|
|
||||||
for _, attr := range v.Attributes() {
|
|
||||||
image.SetAttribute(attr.Name, attr.Value)
|
|
||||||
}
|
|
||||||
for child := v.FirstChild(); child != nil; {
|
|
||||||
next := child.NextSibling()
|
|
||||||
image.AppendChild(image, child)
|
|
||||||
child = next
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append our duplicate image to the wrapper link
|
|
||||||
wrap.AppendChild(wrap, image)
|
|
||||||
|
|
||||||
// Wire in the next sibling
|
|
||||||
wrap.SetNextSibling(next)
|
|
||||||
|
|
||||||
// Replace the current node with the wrapper link
|
|
||||||
parent.ReplaceChild(parent, n, wrap)
|
|
||||||
|
|
||||||
// But most importantly ensure the next sibling is still on the old image too
|
|
||||||
v.SetNextSibling(next)
|
|
||||||
}
|
|
||||||
case *ast.Link:
|
case *ast.Link:
|
||||||
// Links need their href to munged to be a real value
|
g.transformLink(ctx, v, reader)
|
||||||
link := v.Destination
|
|
||||||
isAnchorFragment := len(link) > 0 && link[0] == '#'
|
|
||||||
if !isAnchorFragment && !markup.IsFullURLBytes(link) {
|
|
||||||
base := ctx.Links.Base
|
|
||||||
if ctx.IsWiki {
|
|
||||||
base = ctx.Links.WikiLink()
|
|
||||||
} else if ctx.Links.HasBranchInfo() {
|
|
||||||
base = ctx.Links.SrcLink()
|
|
||||||
}
|
|
||||||
link = []byte(giteautil.URLJoin(base, string(link)))
|
|
||||||
}
|
|
||||||
if isAnchorFragment {
|
|
||||||
link = []byte("#user-content-" + string(link)[1:])
|
|
||||||
}
|
|
||||||
v.Destination = link
|
|
||||||
case *ast.List:
|
case *ast.List:
|
||||||
if v.HasChildren() {
|
g.transformList(ctx, v, reader, rc)
|
||||||
children := make([]ast.Node, 0, v.ChildCount())
|
|
||||||
child := v.FirstChild()
|
|
||||||
for child != nil {
|
|
||||||
children = append(children, child)
|
|
||||||
child = child.NextSibling()
|
|
||||||
}
|
|
||||||
v.RemoveChildren(v)
|
|
||||||
|
|
||||||
for _, child := range children {
|
|
||||||
listItem := child.(*ast.ListItem)
|
|
||||||
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("class", []byte("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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.applyElementDir(v)
|
|
||||||
case *ast.Text:
|
case *ast.Text:
|
||||||
if v.SoftLineBreak() && !v.HardLineBreak() {
|
if v.SoftLineBreak() && !v.HardLineBreak() {
|
||||||
if ctx.Metas["mode"] != "document" {
|
if ctx.Metas["mode"] != "document" {
|
||||||
|
@ -192,10 +79,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case *ast.CodeSpan:
|
case *ast.CodeSpan:
|
||||||
colorContent := n.Text(reader.Source())
|
g.transformCodeSpan(ctx, v, reader)
|
||||||
if css.ColorHandler(strings.ToLower(string(colorContent))) {
|
|
||||||
v.AppendChild(v, NewColorPreview(colorContent))
|
|
||||||
}
|
|
||||||
case *ast.Blockquote:
|
case *ast.Blockquote:
|
||||||
return g.transformBlockquote(v, reader)
|
return g.transformBlockquote(v, reader)
|
||||||
}
|
}
|
||||||
|
@ -219,50 +103,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type prefixedIDs struct {
|
|
||||||
values container.Set[string]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate generates a new element id.
|
|
||||||
func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
|
|
||||||
dft := []byte("id")
|
|
||||||
if kind == ast.KindHeading {
|
|
||||||
dft = []byte("heading")
|
|
||||||
}
|
|
||||||
return p.GenerateWithDefault(value, dft)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateWithDefault generates a new element id.
|
|
||||||
func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
|
|
||||||
result := common.CleanValue(value)
|
|
||||||
if len(result) == 0 {
|
|
||||||
result = dft
|
|
||||||
}
|
|
||||||
if !bytes.HasPrefix(result, []byte("user-content-")) {
|
|
||||||
result = append([]byte("user-content-"), result...)
|
|
||||||
}
|
|
||||||
if p.values.Add(util.BytesToReadOnlyString(result)) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
for i := 1; ; i++ {
|
|
||||||
newResult := fmt.Sprintf("%s-%d", result, i)
|
|
||||||
if p.values.Add(newResult) {
|
|
||||||
return []byte(newResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put puts a given element id to the used ids table.
|
|
||||||
func (p *prefixedIDs) Put(value []byte) {
|
|
||||||
p.values.Add(util.BytesToReadOnlyString(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPrefixedIDs() *prefixedIDs {
|
|
||||||
return &prefixedIDs{
|
|
||||||
values: make(container.Set[string]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHTMLRenderer creates a HTMLRenderer to render
|
// NewHTMLRenderer creates a HTMLRenderer to render
|
||||||
// in the gitea form.
|
// in the gitea form.
|
||||||
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||||
|
@ -295,60 +135,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||||
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
|
|
||||||
// See #21474 for reference
|
|
||||||
func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
||||||
if entering {
|
|
||||||
if n.Attributes() != nil {
|
|
||||||
_, _ = w.WriteString("<code")
|
|
||||||
html.RenderAttributes(w, n, html.CodeAttributeFilter)
|
|
||||||
_ = w.WriteByte('>')
|
|
||||||
} else {
|
|
||||||
_, _ = w.WriteString("<code>")
|
|
||||||
}
|
|
||||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
|
||||||
switch v := c.(type) {
|
|
||||||
case *ast.Text:
|
|
||||||
segment := v.Segment
|
|
||||||
value := segment.Value(source)
|
|
||||||
if bytes.HasSuffix(value, []byte("\n")) {
|
|
||||||
r.Writer.RawWrite(w, value[:len(value)-1])
|
|
||||||
r.Writer.RawWrite(w, []byte(" "))
|
|
||||||
} else {
|
|
||||||
r.Writer.RawWrite(w, value)
|
|
||||||
}
|
|
||||||
case *ColorPreview:
|
|
||||||
_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ast.WalkSkipChildren, nil
|
|
||||||
}
|
|
||||||
_, _ = w.WriteString("</code>")
|
|
||||||
return ast.WalkContinue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
|
|
||||||
func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
||||||
if entering {
|
|
||||||
n := node.(*Attention)
|
|
||||||
var octiconName string
|
|
||||||
switch n.AttentionType {
|
|
||||||
case "tip":
|
|
||||||
octiconName = "light-bulb"
|
|
||||||
case "important":
|
|
||||||
octiconName = "report"
|
|
||||||
case "warning":
|
|
||||||
octiconName = "alert"
|
|
||||||
case "caution":
|
|
||||||
octiconName = "stop"
|
|
||||||
default: // including "note"
|
|
||||||
octiconName = "info"
|
|
||||||
}
|
|
||||||
_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
|
|
||||||
}
|
|
||||||
return ast.WalkContinue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
n := node.(*ast.Document)
|
n := node.(*ast.Document)
|
||||||
|
|
||||||
|
@ -435,38 +221,3 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
|
||||||
|
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
||||||
n := node.(*TaskCheckBoxListItem)
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, _ = w.WriteString("</li>\n")
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
59
modules/markup/markdown/prefixed_id.go
Normal file
59
modules/markup/markdown/prefixed_id.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
|
"code.gitea.io/gitea/modules/markup/common"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type prefixedIDs struct {
|
||||||
|
values container.Set[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate generates a new element id.
|
||||||
|
func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
|
||||||
|
dft := []byte("id")
|
||||||
|
if kind == ast.KindHeading {
|
||||||
|
dft = []byte("heading")
|
||||||
|
}
|
||||||
|
return p.GenerateWithDefault(value, dft)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateWithDefault generates a new element id.
|
||||||
|
func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
|
||||||
|
result := common.CleanValue(value)
|
||||||
|
if len(result) == 0 {
|
||||||
|
result = dft
|
||||||
|
}
|
||||||
|
if !bytes.HasPrefix(result, []byte("user-content-")) {
|
||||||
|
result = append([]byte("user-content-"), result...)
|
||||||
|
}
|
||||||
|
if p.values.Add(util.BytesToReadOnlyString(result)) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for i := 1; ; i++ {
|
||||||
|
newResult := fmt.Sprintf("%s-%d", result, i)
|
||||||
|
if p.values.Add(newResult) {
|
||||||
|
return []byte(newResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put puts a given element id to the used ids table.
|
||||||
|
func (p *prefixedIDs) Put(value []byte) {
|
||||||
|
p.values.Add(util.BytesToReadOnlyString(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrefixedIDs() *prefixedIDs {
|
||||||
|
return &prefixedIDs{
|
||||||
|
values: make(container.Set[string]),
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,12 +6,37 @@ package markdown
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/svg"
|
||||||
|
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
"github.com/yuin/goldmark/text"
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
|
||||||
|
func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
if entering {
|
||||||
|
n := node.(*Attention)
|
||||||
|
var octiconName string
|
||||||
|
switch n.AttentionType {
|
||||||
|
case "tip":
|
||||||
|
octiconName = "light-bulb"
|
||||||
|
case "important":
|
||||||
|
octiconName = "report"
|
||||||
|
case "warning":
|
||||||
|
octiconName = "alert"
|
||||||
|
case "caution":
|
||||||
|
octiconName = "stop"
|
||||||
|
default: // including "note"
|
||||||
|
octiconName = "info"
|
||||||
|
}
|
||||||
|
_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
|
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
|
||||||
// We only want attention blockquotes when the AST looks like:
|
// We only want attention blockquotes when the AST looks like:
|
||||||
// > Text("[") Text("!TYPE") Text("]")
|
// > Text("[") Text("!TYPE") Text("]")
|
||||||
|
@ -43,7 +68,7 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
|
||||||
|
|
||||||
// grab attention type from markdown source
|
// grab attention type from markdown source
|
||||||
attentionType := strings.ToLower(val2[1:])
|
attentionType := strings.ToLower(val2[1:])
|
||||||
if !g.AttentionTypes.Contains(attentionType) {
|
if !g.attentionTypes.Contains(attentionType) {
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
57
modules/markup/markdown/transform_codespan.go
Normal file
57
modules/markup/markdown/transform_codespan.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
|
||||||
|
"github.com/microcosm-cc/bluemonday/css"
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
|
||||||
|
// See #21474 for reference
|
||||||
|
func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
if entering {
|
||||||
|
if n.Attributes() != nil {
|
||||||
|
_, _ = w.WriteString("<code")
|
||||||
|
html.RenderAttributes(w, n, html.CodeAttributeFilter)
|
||||||
|
_ = w.WriteByte('>')
|
||||||
|
} else {
|
||||||
|
_, _ = w.WriteString("<code>")
|
||||||
|
}
|
||||||
|
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
|
switch v := c.(type) {
|
||||||
|
case *ast.Text:
|
||||||
|
segment := v.Segment
|
||||||
|
value := segment.Value(source)
|
||||||
|
if bytes.HasSuffix(value, []byte("\n")) {
|
||||||
|
r.Writer.RawWrite(w, value[:len(value)-1])
|
||||||
|
r.Writer.RawWrite(w, []byte(" "))
|
||||||
|
} else {
|
||||||
|
r.Writer.RawWrite(w, value)
|
||||||
|
}
|
||||||
|
case *ColorPreview:
|
||||||
|
_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast.WalkSkipChildren, nil
|
||||||
|
}
|
||||||
|
_, _ = w.WriteString("</code>")
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
|
||||||
|
colorContent := v.Text(reader.Source())
|
||||||
|
if css.ColorHandler(strings.ToLower(string(colorContent))) {
|
||||||
|
v.AppendChild(v, NewColorPreview(colorContent))
|
||||||
|
}
|
||||||
|
}
|
32
modules/markup/markdown/transform_heading.go
Normal file
32
modules/markup/markdown/transform_heading.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *ASTTransformer) transformHeading(ctx *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) {
|
||||||
|
for _, attr := range v.Attributes() {
|
||||||
|
if _, ok := attr.Value.([]byte); !ok {
|
||||||
|
v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txt := v.Text(reader.Source())
|
||||||
|
header := markup.Header{
|
||||||
|
Text: util.BytesToReadOnlyString(txt),
|
||||||
|
Level: v.Level,
|
||||||
|
}
|
||||||
|
if id, found := v.AttributeString("id"); found {
|
||||||
|
header.ID = util.BytesToReadOnlyString(id.([]byte))
|
||||||
|
}
|
||||||
|
*tocList = append(*tocList, header)
|
||||||
|
g.applyElementDir(v)
|
||||||
|
}
|
66
modules/markup/markdown/transform_image.go
Normal file
66
modules/markup/markdown/transform_image.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
giteautil "code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image, reader text.Reader) {
|
||||||
|
// Images need two things:
|
||||||
|
//
|
||||||
|
// 1. Their src needs to munged to be a real value
|
||||||
|
// 2. If they're not wrapped with a link they need a link wrapper
|
||||||
|
|
||||||
|
// Check if the destination is a real link
|
||||||
|
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
|
||||||
|
v.Destination = []byte(giteautil.URLJoin(
|
||||||
|
ctx.Links.ResolveMediaLink(ctx.IsWiki),
|
||||||
|
strings.TrimLeft(string(v.Destination), "/"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := v.Parent()
|
||||||
|
// Create a link around image only if parent is not already a link
|
||||||
|
if _, ok := parent.(*ast.Link); !ok && parent != nil {
|
||||||
|
next := v.NextSibling()
|
||||||
|
|
||||||
|
// Create a link wrapper
|
||||||
|
wrap := ast.NewLink()
|
||||||
|
wrap.Destination = v.Destination
|
||||||
|
wrap.Title = v.Title
|
||||||
|
wrap.SetAttributeString("target", []byte("_blank"))
|
||||||
|
|
||||||
|
// Duplicate the current image node
|
||||||
|
image := ast.NewImage(ast.NewLink())
|
||||||
|
image.Destination = v.Destination
|
||||||
|
image.Title = v.Title
|
||||||
|
for _, attr := range v.Attributes() {
|
||||||
|
image.SetAttribute(attr.Name, attr.Value)
|
||||||
|
}
|
||||||
|
for child := v.FirstChild(); child != nil; {
|
||||||
|
next := child.NextSibling()
|
||||||
|
image.AppendChild(image, child)
|
||||||
|
child = next
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append our duplicate image to the wrapper link
|
||||||
|
wrap.AppendChild(wrap, image)
|
||||||
|
|
||||||
|
// Wire in the next sibling
|
||||||
|
wrap.SetNextSibling(next)
|
||||||
|
|
||||||
|
// Replace the current node with the wrapper link
|
||||||
|
parent.ReplaceChild(parent, v, wrap)
|
||||||
|
|
||||||
|
// But most importantly ensure the next sibling is still on the old image too
|
||||||
|
v.SetNextSibling(next)
|
||||||
|
}
|
||||||
|
}
|
31
modules/markup/markdown/transform_link.go
Normal file
31
modules/markup/markdown/transform_link.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
giteautil "code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) {
|
||||||
|
// Links need their href to munged to be a real value
|
||||||
|
link := v.Destination
|
||||||
|
isAnchorFragment := len(link) > 0 && link[0] == '#'
|
||||||
|
if !isAnchorFragment && !markup.IsFullURLBytes(link) {
|
||||||
|
base := ctx.Links.Base
|
||||||
|
if ctx.IsWiki {
|
||||||
|
base = ctx.Links.WikiLink()
|
||||||
|
} else if ctx.Links.HasBranchInfo() {
|
||||||
|
base = ctx.Links.SrcLink()
|
||||||
|
}
|
||||||
|
link = []byte(giteautil.URLJoin(base, string(link)))
|
||||||
|
}
|
||||||
|
if isAnchorFragment {
|
||||||
|
link = []byte("#user-content-" + string(link)[1:])
|
||||||
|
}
|
||||||
|
v.Destination = link
|
||||||
|
}
|
86
modules/markup/markdown/transform_list.go
Normal file
86
modules/markup/markdown/transform_list.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
east "github.com/yuin/goldmark/extension/ast"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"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)
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, _ = w.WriteString("</li>\n")
|
||||||
|
}
|
||||||
|
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(ctx *markup.RenderContext, v *ast.List, reader text.Reader, 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()
|
||||||
|
}
|
||||||
|
v.RemoveChildren(v)
|
||||||
|
|
||||||
|
for _, child := range children {
|
||||||
|
listItem := child.(*ast.ListItem)
|
||||||
|
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("class", []byte("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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.applyElementDir(v)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user