This commit is contained in:
@@ -95,13 +95,13 @@ type Icon struct {
|
||||
// ColorPreview is an inline for a color preview
|
||||
type ColorPreview struct {
|
||||
ast.BaseInline
|
||||
Color []byte
|
||||
Color string
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *ColorPreview) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Color"] = string(n.Color)
|
||||
m["Color"] = n.Color
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func (n *ColorPreview) Kind() ast.NodeKind {
|
||||
}
|
||||
|
||||
// NewColorPreview returns a new Span node.
|
||||
func NewColorPreview(color []byte) *ColorPreview {
|
||||
func NewColorPreview(color string) *ColorPreview {
|
||||
return &ColorPreview{
|
||||
BaseInline: ast.BaseInline{},
|
||||
Color: color,
|
||||
@@ -170,3 +170,14 @@ func (n *RawHTML) Kind() ast.NodeKind {
|
||||
func NewRawHTML(rawHTML template.HTML) *RawHTML {
|
||||
return &RawHTML{rawHTML: rawHTML}
|
||||
}
|
||||
|
||||
func childSingleText(node ast.Node, source []byte) (string, bool) {
|
||||
if node.FirstChild() == nil || node.FirstChild() != node.LastChild() {
|
||||
return "", false
|
||||
}
|
||||
c, ok := node.FirstChild().(*ast.Text)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return string(c.Segment.Value(source)), true
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ package markdown
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"gitea.dev/modules/htmlutil"
|
||||
"gitea.dev/modules/svg"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func nodeToTable(meta *yaml.Node) ast.Node {
|
||||
|
||||
@@ -6,9 +6,9 @@ package markdown
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/internal"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
@@ -70,6 +70,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
}
|
||||
case *ast.CodeSpan:
|
||||
g.transformCodeSpan(ctx, v, reader)
|
||||
case *ast.FencedCodeBlock:
|
||||
g.transformFencedCodeblock(v, reader)
|
||||
case *ast.Blockquote:
|
||||
return g.transformBlockquote(v, reader)
|
||||
}
|
||||
@@ -117,12 +119,33 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(KindDetails, r.renderDetails)
|
||||
reg.Register(KindSummary, r.renderSummary)
|
||||
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)
|
||||
}
|
||||
|
||||
// renderCodeBlock wraps indented code blocks like the fenced renderer
|
||||
func (r *HTMLRenderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
opening := r.renderInternal.ProtectSafeAttrs(`<div class="code-block-container code-overflow-scroll"><pre class="code-block"><code>`)
|
||||
if _, err := w.WriteString(string(opening)); err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
lines := n.Lines()
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
line := lines.At(i)
|
||||
r.Writer.RawWrite(w, line.Value(source))
|
||||
}
|
||||
} else {
|
||||
if _, err := w.WriteString("</code></pre></div>"); err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Document)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -11,12 +11,13 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/common"
|
||||
"code.gitea.io/gitea/modules/markup/markdown/math"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
giteautil "code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/htmlutil"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/common"
|
||||
"gitea.dev/modules/markup/markdown/math"
|
||||
"gitea.dev/modules/setting"
|
||||
giteautil "gitea.dev/modules/util"
|
||||
|
||||
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/yuin/goldmark"
|
||||
@@ -64,21 +65,17 @@ func newParserContext(ctx *markup.RenderContext) parser.Context {
|
||||
return pc
|
||||
}
|
||||
|
||||
type GlodmarkRender struct {
|
||||
type GoldmarkRender struct {
|
||||
ctx *markup.RenderContext
|
||||
|
||||
goldmarkMarkdown goldmark.Markdown
|
||||
}
|
||||
|
||||
func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error {
|
||||
func (r *GoldmarkRender) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error {
|
||||
return r.goldmarkMarkdown.Convert(source, writer, opts...)
|
||||
}
|
||||
|
||||
func (r *GlodmarkRender) Renderer() renderer.Renderer {
|
||||
return r.goldmarkMarkdown.Renderer()
|
||||
}
|
||||
|
||||
func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||
func (r *GoldmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||
if entering {
|
||||
languageBytes, _ := c.Language()
|
||||
languageStr := giteautil.IfZero(string(languageBytes), "text")
|
||||
@@ -139,10 +136,10 @@ func goldmarkDefaultParser() parser.Parser {
|
||||
}
|
||||
|
||||
// SpecializedMarkdown sets up the Gitea specific markdown extensions
|
||||
func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
|
||||
func SpecializedMarkdown(ctx *markup.RenderContext) *GoldmarkRender {
|
||||
// TODO: it could use a pool to cache the renderers to reuse them with different contexts
|
||||
// at the moment it is fast enough (see the benchmarks)
|
||||
r := &GlodmarkRender{ctx: ctx}
|
||||
r := &GoldmarkRender{ctx: ctx}
|
||||
r.goldmarkMarkdown = goldmark.New(
|
||||
goldmark.WithParser(goldmarkDefaultParser()),
|
||||
goldmark.WithExtensions(
|
||||
@@ -272,7 +269,7 @@ func RenderString(ctx *markup.RenderContext, content string) (template.HTML, err
|
||||
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
|
||||
log.Warn("Unable to RenderString: %v, content: %s", err, giteautil.TruncateRunes(content, 200))
|
||||
err = nil
|
||||
return template.HTML(template.HTMLEscapeString(content)), err
|
||||
return htmlutil.EscapeString(content), err
|
||||
}
|
||||
return template.HTML(buf.String()), nil
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/svg"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/cases"
|
||||
|
||||
@@ -6,8 +6,8 @@ package markdown_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
)
|
||||
|
||||
func BenchmarkSpecializedMarkdown(b *testing.B) {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -600,3 +600,24 @@ func TestMarkdownUlDir(t *testing.T) {
|
||||
</ul>
|
||||
`, string(result))
|
||||
}
|
||||
|
||||
func TestMarkdownCodeBlock(t *testing.T) {
|
||||
testRender := func(input, expected string) {
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
}
|
||||
const nl = "\n"
|
||||
const prefix = `<div class="code-block-container code-overflow-scroll"><pre class="code-block">`
|
||||
const suffix = `</pre></div>`
|
||||
|
||||
testRender("```\ncode\n```", prefix+`<code class="chroma language-text display">code`+nl+`</code>`+suffix)
|
||||
|
||||
const jsCommon = prefix + `<code class="chroma language-js display"><span class="nx">code</span>` + nl + `</code>` + suffix
|
||||
testRender("```js\ncode\n```", jsCommon)
|
||||
testRender("```js:app.ts\ncode\n```", jsCommon)
|
||||
testRender("```js,ignore\ncode\n```", jsCommon)
|
||||
testRender("```js ignore\ncode\n```", jsCommon)
|
||||
testRender(" code\n", prefix+`<code>code`+nl+`</code>`+suffix)
|
||||
testRender(" <script>alert(1)</script>\n", prefix+`<code><script>alert(1)</script>`+nl+`</code>`+suffix)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ package math
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
giteaUtil "code.gitea.io/gitea/modules/util"
|
||||
giteaUtil "gitea.dev/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
|
||||
@@ -6,8 +6,8 @@ package math
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
giteaUtil "code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/markup/internal"
|
||||
giteaUtil "gitea.dev/modules/util"
|
||||
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
|
||||
@@ -6,7 +6,7 @@ package math
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
"gitea.dev/modules/markup/internal"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
package math
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
giteaUtil "code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/markup/internal"
|
||||
giteaUtil "gitea.dev/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func isYAMLSeparator(line []byte) bool {
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"gitea.dev/modules/markup"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// RenderConfig represents rendering configuration for this file
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
|
||||
@@ -6,7 +6,7 @@ package markdown
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"gitea.dev/modules/svg"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
@@ -46,7 +46,10 @@ func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.N
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
val1 := string(node1.Text(reader.Source())) //nolint:staticcheck // Text is deprecated
|
||||
val1, ok := childSingleText(node1, reader.Source())
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
attentionType := strings.ToLower(val1)
|
||||
if g.attentionTypes.Contains(attentionType) {
|
||||
return attentionType, []ast.Node{node1}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
func (g *ASTTransformer) transformFencedCodeblock(v *ast.FencedCodeBlock, reader text.Reader) {
|
||||
// * Some engines support a meta syntax for appending the filename after the language, separated by a colon
|
||||
// * https://www.glukhov.org/documentation-tools/markdown/markdown-codeblocks/
|
||||
// * Some engines support additional "options" after the language, separated by a space or comma: ```rust,ignore```
|
||||
// * https://docs.readme.com/rdmd/docs/code-blocks
|
||||
// * https://next-book.vercel.app/reference/fencedcode
|
||||
if v.Info == nil {
|
||||
return
|
||||
}
|
||||
info := v.Info.Segment.Value(reader.Source())
|
||||
newEnd := -1
|
||||
for i, b := range info {
|
||||
if b == ' ' || b == ',' || b == ':' {
|
||||
newEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if newEnd != -1 {
|
||||
start := v.Info.Segment.Start
|
||||
v.Info = ast.NewTextSegment(text.NewSegment(start, start+newEnd))
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"gitea.dev/modules/markup"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday/css"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
@@ -39,7 +39,7 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
|
||||
r.Writer.RawWrite(w, value)
|
||||
}
|
||||
case *ColorPreview:
|
||||
_ = r.renderInternal.FormatWithSafeAttrs(w, `<span class="color-preview" style="background-color: %s"></span>`, string(v.Color))
|
||||
_ = r.renderInternal.FormatWithSafeAttrs(w, `<span class="color-preview" style="background-color: %s"></span>`, v.Color)
|
||||
}
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
@@ -68,8 +68,11 @@ func cssColorHandler(value string) bool {
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) transformCodeSpan(_ *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
|
||||
colorContent := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated
|
||||
if cssColorHandler(string(colorContent)) {
|
||||
colorContent, ok := childSingleText(v, reader.Source())
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if cssColorHandler(colorContent) {
|
||||
v.AppendChild(v, NewColorPreview(colorContent))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ package markdown
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"gitea.dev/modules/markup"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
|
||||
Reference in New Issue
Block a user