Add RTL rendering support to Markdown (#24816)
Support RTL content in Markdown: ![image](https://github.com/go-gitea/gitea/assets/115237/dedb1b0c-2f05-40dc-931a-0d9dc81f7c97) Example document: https://try.gitea.io/silverwind/symlink-test/src/branch/master/bidi-text.md Same on GitHub: https://github.com/silverwind/symlink-test/blob/master/bidi-text.md `dir=auto` enables a browser heuristic that sets the text direction automatically. It is the only way to get automatic text direction. Ref: https://codeberg.org/Codeberg/Community/issues/1021 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
1698c15cba
commit
32d9c47ec7
|
@ -630,7 +630,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
}
|
}
|
||||||
mentionedUsername := mention[1:]
|
mentionedUsername := mention[1:]
|
||||||
|
|
||||||
if processorHelper.IsUsernameMentionable != nil && processorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
|
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
|
||||||
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention"))
|
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention"))
|
||||||
node = node.NextSibling.NextSibling
|
node = node.NextSibling.NextSibling
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -47,6 +47,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||||
tocMode = rc.TOC
|
tocMode = rc.TOC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyElementDir := func(n ast.Node) {
|
||||||
|
if markup.DefaultProcessorHelper.ElementDir != "" {
|
||||||
|
n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
|
attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
|
||||||
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
if !entering {
|
if !entering {
|
||||||
|
@ -69,6 +75,9 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||||
header.ID = util.BytesToReadOnlyString(id.([]byte))
|
header.ID = util.BytesToReadOnlyString(id.([]byte))
|
||||||
}
|
}
|
||||||
tocList = append(tocList, header)
|
tocList = append(tocList, header)
|
||||||
|
applyElementDir(v)
|
||||||
|
case *ast.Paragraph:
|
||||||
|
applyElementDir(v)
|
||||||
case *ast.Image:
|
case *ast.Image:
|
||||||
// Images need two things:
|
// Images need two things:
|
||||||
//
|
//
|
||||||
|
@ -171,6 +180,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||||
v.AppendChild(v, newChild)
|
v.AppendChild(v, newChild)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyElementDir(v)
|
||||||
case *ast.Text:
|
case *ast.Text:
|
||||||
if v.SoftLineBreak() && !v.HardLineBreak() {
|
if v.SoftLineBreak() && !v.HardLineBreak() {
|
||||||
renderMetas := pc.Get(renderMetasKey).(map[string]string)
|
renderMetas := pc.Get(renderMetasKey).(map[string]string)
|
||||||
|
|
|
@ -30,14 +30,16 @@ const (
|
||||||
|
|
||||||
type ProcessorHelper struct {
|
type ProcessorHelper struct {
|
||||||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||||
|
|
||||||
|
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
var processorHelper ProcessorHelper
|
var DefaultProcessorHelper ProcessorHelper
|
||||||
|
|
||||||
// Init initialize regexps for markdown parsing
|
// Init initialize regexps for markdown parsing
|
||||||
func Init(ph *ProcessorHelper) {
|
func Init(ph *ProcessorHelper) {
|
||||||
if ph != nil {
|
if ph != nil {
|
||||||
processorHelper = *ph
|
DefaultProcessorHelper = *ph
|
||||||
}
|
}
|
||||||
|
|
||||||
NewSanitizer()
|
NewSanitizer()
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
func ProcessorHelper() *markup.ProcessorHelper {
|
func ProcessorHelper() *markup.ProcessorHelper {
|
||||||
return &markup.ProcessorHelper{
|
return &markup.ProcessorHelper{
|
||||||
|
ElementDir: "auto", // set dir="auto" for necessary (eg: <p>, <h?>, etc) tags
|
||||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||||
mentionedUser, err := user.GetUserByName(ctx, username)
|
mentionedUser, err := user.GetUserByName(ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -250,7 +250,7 @@ func TestGetUserRss(t *testing.T) {
|
||||||
title, _ := rssDoc.ChildrenFiltered("title").Html()
|
title, _ := rssDoc.ChildrenFiltered("title").Html()
|
||||||
assert.EqualValues(t, "Feed of "the_1-user.with.all.allowedChars"", title)
|
assert.EqualValues(t, "Feed of "the_1-user.with.all.allowedChars"", title)
|
||||||
description, _ := rssDoc.ChildrenFiltered("description").Html()
|
description, _ := rssDoc.ChildrenFiltered("description").Html()
|
||||||
assert.EqualValues(t, "<p>some <a href="https://commonmark.org/" rel="nofollow">commonmark</a>!</p>\n", description)
|
assert.EqualValues(t, "<p dir="auto">some <a href="https://commonmark.org/" rel="nofollow">commonmark</a>!</p>\n", description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1091,6 +1091,7 @@ a.label,
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background: var(--color-box-body);
|
background: var(--color-box-body);
|
||||||
border-color: var(--color-secondary);
|
border-color: var(--color-secondary);
|
||||||
|
text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.table th,
|
.ui.table th,
|
||||||
|
|
|
@ -23,9 +23,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.markup .anchor {
|
.markup .anchor {
|
||||||
|
float: left;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
margin-left: -20px;
|
margin-left: -20px;
|
||||||
line-height: 1;
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,10 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markup h1 .anchor {
|
||||||
|
margin-top: -2px; /* re-align to center */
|
||||||
|
}
|
||||||
|
|
||||||
.markup h1 .anchor .svg,
|
.markup h1 .anchor .svg,
|
||||||
.markup h2 .anchor .svg,
|
.markup h2 .anchor .svg,
|
||||||
.markup h3 .anchor .svg,
|
.markup h3 .anchor .svg,
|
||||||
|
|
Loading…
Reference in a new issue