mirror of
https://gitea.com/gitea/tea.git
synced 2024-11-27 04:51:41 +01:00
d2295828d0
Path-only URLs need an absolute reference to be resolved against for printing in markdown Previously we resolved against the URL to the resource we were operating on (eg comment or issue URL). The markdown renderer in the web UI resolves all such URLs relative to the repo base URL. This PR adopts this behaviour in tea, by trimming the URL to a repo base URL via regex. This makes a custom patch to our markdown renderer `glamour` obsolete, which turned out to be an incorrect patch, meaning we can make use of upstream glamour again. Co-authored-by: Norwin <git@nroo.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/401 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-by: 6543 <6543@obermui.de> Co-authored-by: Norwin <noerw@noreply.gitea.io> Co-committed-by: Norwin <noerw@noreply.gitea.io>
411 lines
8.3 KiB
Go
411 lines
8.3 KiB
Go
package ansi
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html"
|
|
"io"
|
|
"strings"
|
|
|
|
east "github.com/yuin/goldmark-emoji/ast"
|
|
"github.com/yuin/goldmark/ast"
|
|
astext "github.com/yuin/goldmark/extension/ast"
|
|
)
|
|
|
|
// ElementRenderer is called when entering a markdown node.
|
|
type ElementRenderer interface {
|
|
Render(w io.Writer, ctx RenderContext) error
|
|
}
|
|
|
|
// ElementFinisher is called when leaving a markdown node.
|
|
type ElementFinisher interface {
|
|
Finish(w io.Writer, ctx RenderContext) error
|
|
}
|
|
|
|
// An Element is used to instruct the renderer how to handle individual markdown
|
|
// nodes.
|
|
type Element struct {
|
|
Entering string
|
|
Exiting string
|
|
Renderer ElementRenderer
|
|
Finisher ElementFinisher
|
|
}
|
|
|
|
// NewElement returns the appropriate render Element for a given node.
|
|
func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
|
|
ctx := tr.context
|
|
// fmt.Print(strings.Repeat(" ", ctx.blockStack.Len()), node.Type(), node.Kind())
|
|
// defer fmt.Println()
|
|
|
|
switch node.Kind() {
|
|
// Document
|
|
case ast.KindDocument:
|
|
e := &BlockElement{
|
|
Block: &bytes.Buffer{},
|
|
Style: ctx.options.Styles.Document,
|
|
Margin: true,
|
|
}
|
|
return Element{
|
|
Renderer: e,
|
|
Finisher: e,
|
|
}
|
|
|
|
// Heading
|
|
case ast.KindHeading:
|
|
n := node.(*ast.Heading)
|
|
he := &HeadingElement{
|
|
Level: n.Level,
|
|
First: node.PreviousSibling() == nil,
|
|
}
|
|
return Element{
|
|
Exiting: "",
|
|
Renderer: he,
|
|
Finisher: he,
|
|
}
|
|
|
|
// Paragraph
|
|
case ast.KindParagraph:
|
|
if node.Parent() != nil && node.Parent().Kind() == ast.KindListItem {
|
|
return Element{}
|
|
}
|
|
return Element{
|
|
Renderer: &ParagraphElement{
|
|
First: node.PreviousSibling() == nil,
|
|
},
|
|
Finisher: &ParagraphElement{},
|
|
}
|
|
|
|
// Blockquote
|
|
case ast.KindBlockquote:
|
|
e := &BlockElement{
|
|
Block: &bytes.Buffer{},
|
|
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.BlockQuote, false),
|
|
Margin: true,
|
|
Newline: true,
|
|
}
|
|
return Element{
|
|
Entering: "\n",
|
|
Renderer: e,
|
|
Finisher: e,
|
|
}
|
|
|
|
// Lists
|
|
case ast.KindList:
|
|
s := ctx.options.Styles.List.StyleBlock
|
|
if s.Indent == nil {
|
|
var i uint
|
|
s.Indent = &i
|
|
}
|
|
n := node.Parent()
|
|
for n != nil {
|
|
if n.Kind() == ast.KindList {
|
|
i := ctx.options.Styles.List.LevelIndent
|
|
s.Indent = &i
|
|
break
|
|
}
|
|
n = n.Parent()
|
|
}
|
|
|
|
e := &BlockElement{
|
|
Block: &bytes.Buffer{},
|
|
Style: cascadeStyle(ctx.blockStack.Current().Style, s, false),
|
|
Margin: true,
|
|
Newline: true,
|
|
}
|
|
return Element{
|
|
Entering: "\n",
|
|
Renderer: e,
|
|
Finisher: e,
|
|
}
|
|
|
|
case ast.KindListItem:
|
|
var l uint
|
|
var e uint
|
|
l = 1
|
|
n := node
|
|
for n.PreviousSibling() != nil && (n.PreviousSibling().Kind() == ast.KindListItem) {
|
|
l++
|
|
n = n.PreviousSibling()
|
|
}
|
|
if node.Parent().(*ast.List).IsOrdered() {
|
|
e = l
|
|
}
|
|
|
|
post := "\n"
|
|
if (node.LastChild() != nil && node.LastChild().Kind() == ast.KindList) ||
|
|
node.NextSibling() == nil {
|
|
post = ""
|
|
}
|
|
|
|
if node.FirstChild() != nil &&
|
|
node.FirstChild().FirstChild() != nil &&
|
|
node.FirstChild().FirstChild().Kind() == astext.KindTaskCheckBox {
|
|
nc := node.FirstChild().FirstChild().(*astext.TaskCheckBox)
|
|
|
|
return Element{
|
|
Exiting: post,
|
|
Renderer: &TaskElement{
|
|
Checked: nc.IsChecked,
|
|
},
|
|
}
|
|
}
|
|
|
|
return Element{
|
|
Exiting: post,
|
|
Renderer: &ItemElement{
|
|
Enumeration: e,
|
|
},
|
|
}
|
|
|
|
// Text Elements
|
|
case ast.KindText:
|
|
n := node.(*ast.Text)
|
|
s := string(n.Segment.Value(source))
|
|
|
|
if n.HardLineBreak() || (n.SoftLineBreak()) {
|
|
s += "\n"
|
|
}
|
|
return Element{
|
|
Renderer: &BaseElement{
|
|
Token: html.UnescapeString(s),
|
|
Style: ctx.options.Styles.Text,
|
|
},
|
|
}
|
|
|
|
case ast.KindEmphasis:
|
|
n := node.(*ast.Emphasis)
|
|
s := string(n.Text(source))
|
|
style := ctx.options.Styles.Emph
|
|
if n.Level > 1 {
|
|
style = ctx.options.Styles.Strong
|
|
}
|
|
|
|
return Element{
|
|
Renderer: &BaseElement{
|
|
Token: html.UnescapeString(s),
|
|
Style: style,
|
|
},
|
|
}
|
|
|
|
case astext.KindStrikethrough:
|
|
n := node.(*astext.Strikethrough)
|
|
s := string(n.Text(source))
|
|
style := ctx.options.Styles.Strikethrough
|
|
|
|
return Element{
|
|
Renderer: &BaseElement{
|
|
Token: html.UnescapeString(s),
|
|
Style: style,
|
|
},
|
|
}
|
|
|
|
case ast.KindThematicBreak:
|
|
return Element{
|
|
Entering: "",
|
|
Exiting: "",
|
|
Renderer: &BaseElement{
|
|
Style: ctx.options.Styles.HorizontalRule,
|
|
},
|
|
}
|
|
|
|
// Links
|
|
case ast.KindLink:
|
|
n := node.(*ast.Link)
|
|
return Element{
|
|
Renderer: &LinkElement{
|
|
Text: textFromChildren(node, source),
|
|
BaseURL: ctx.options.BaseURL,
|
|
URL: string(n.Destination),
|
|
},
|
|
}
|
|
case ast.KindAutoLink:
|
|
n := node.(*ast.AutoLink)
|
|
u := string(n.URL(source))
|
|
label := string(n.Label(source))
|
|
if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(u), "mailto:") {
|
|
u = "mailto:" + u
|
|
}
|
|
|
|
return Element{
|
|
Renderer: &LinkElement{
|
|
Text: label,
|
|
BaseURL: ctx.options.BaseURL,
|
|
URL: u,
|
|
},
|
|
}
|
|
|
|
// Images
|
|
case ast.KindImage:
|
|
n := node.(*ast.Image)
|
|
text := string(n.Text(source))
|
|
return Element{
|
|
Renderer: &ImageElement{
|
|
Text: text,
|
|
BaseURL: ctx.options.BaseURL,
|
|
URL: string(n.Destination),
|
|
},
|
|
}
|
|
|
|
// Code
|
|
case ast.KindFencedCodeBlock:
|
|
n := node.(*ast.FencedCodeBlock)
|
|
l := n.Lines().Len()
|
|
s := ""
|
|
for i := 0; i < l; i++ {
|
|
line := n.Lines().At(i)
|
|
s += string(line.Value(source))
|
|
}
|
|
return Element{
|
|
Entering: "\n",
|
|
Renderer: &CodeBlockElement{
|
|
Code: s,
|
|
Language: string(n.Language(source)),
|
|
},
|
|
}
|
|
|
|
case ast.KindCodeBlock:
|
|
n := node.(*ast.CodeBlock)
|
|
l := n.Lines().Len()
|
|
s := ""
|
|
for i := 0; i < l; i++ {
|
|
line := n.Lines().At(i)
|
|
s += string(line.Value(source))
|
|
}
|
|
return Element{
|
|
Entering: "\n",
|
|
Renderer: &CodeBlockElement{
|
|
Code: s,
|
|
},
|
|
}
|
|
|
|
case ast.KindCodeSpan:
|
|
// n := node.(*ast.CodeSpan)
|
|
e := &BlockElement{
|
|
Block: &bytes.Buffer{},
|
|
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.Code, false),
|
|
}
|
|
return Element{
|
|
Renderer: e,
|
|
Finisher: e,
|
|
}
|
|
|
|
// Tables
|
|
case astext.KindTable:
|
|
te := &TableElement{}
|
|
return Element{
|
|
Entering: "\n",
|
|
Renderer: te,
|
|
Finisher: te,
|
|
}
|
|
|
|
case astext.KindTableCell:
|
|
s := ""
|
|
n := node.FirstChild()
|
|
for n != nil {
|
|
s += string(n.Text(source))
|
|
// s += string(n.LinkData.Destination)
|
|
n = n.NextSibling()
|
|
}
|
|
|
|
return Element{
|
|
Renderer: &TableCellElement{
|
|
Text: s,
|
|
Head: node.Parent().Kind() == astext.KindTableHeader,
|
|
},
|
|
}
|
|
|
|
case astext.KindTableHeader:
|
|
return Element{
|
|
Finisher: &TableHeadElement{},
|
|
}
|
|
case astext.KindTableRow:
|
|
return Element{
|
|
Finisher: &TableRowElement{},
|
|
}
|
|
|
|
// HTML Elements
|
|
case ast.KindHTMLBlock:
|
|
n := node.(*ast.HTMLBlock)
|
|
return Element{
|
|
Renderer: &BaseElement{
|
|
Token: ctx.SanitizeHTML(string(n.Text(source)), true) + "\n",
|
|
Style: ctx.options.Styles.HTMLBlock.StylePrimitive,
|
|
},
|
|
}
|
|
case ast.KindRawHTML:
|
|
n := node.(*ast.RawHTML)
|
|
return Element{
|
|
Renderer: &BaseElement{
|
|
Token: ctx.SanitizeHTML(string(n.Text(source)), true),
|
|
Style: ctx.options.Styles.HTMLSpan.StylePrimitive,
|
|
},
|
|
}
|
|
|
|
// Definition Lists
|
|
case astext.KindDefinitionList:
|
|
e := &BlockElement{
|
|
Block: &bytes.Buffer{},
|
|
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.DefinitionList, false),
|
|
Margin: true,
|
|
Newline: true,
|
|
}
|
|
return Element{
|
|
Entering: "\n",
|
|
Renderer: e,
|
|
Finisher: e,
|
|
}
|
|
|
|
case astext.KindDefinitionTerm:
|
|
return Element{
|
|
Renderer: &BaseElement{
|
|
Style: ctx.options.Styles.DefinitionTerm,
|
|
},
|
|
}
|
|
|
|
case astext.KindDefinitionDescription:
|
|
return Element{
|
|
Renderer: &BaseElement{
|
|
Style: ctx.options.Styles.DefinitionDescription,
|
|
},
|
|
}
|
|
|
|
// Handled by parents
|
|
case astext.KindTaskCheckBox:
|
|
// handled by KindListItem
|
|
return Element{}
|
|
case ast.KindTextBlock:
|
|
return Element{}
|
|
|
|
case east.KindEmoji:
|
|
n := node.(*east.Emoji)
|
|
return Element{
|
|
Renderer: &BaseElement{
|
|
Token: string(n.Value.Unicode),
|
|
},
|
|
}
|
|
|
|
// Unknown case
|
|
default:
|
|
fmt.Println("Warning: unhandled element", node.Kind().String())
|
|
return Element{}
|
|
}
|
|
}
|
|
|
|
func textFromChildren(node ast.Node, source []byte) string {
|
|
var s string
|
|
for c := node.FirstChild(); c != nil; c = c.NextSibling() {
|
|
if c.Kind() == ast.KindText {
|
|
cn := c.(*ast.Text)
|
|
s += string(cn.Segment.Value(source))
|
|
|
|
if cn.HardLineBreak() || (cn.SoftLineBreak()) {
|
|
s += "\n"
|
|
}
|
|
} else {
|
|
s += string(c.Text(source))
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|