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:
silverwind 2023-05-20 23:02:52 +02:00 committed by GitHub
parent 1698c15cba
commit 32d9c47ec7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 23 additions and 5 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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()

View file

@ -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 {

View file

@ -250,7 +250,7 @@ func TestGetUserRss(t *testing.T) {
title, _ := rssDoc.ChildrenFiltered("title").Html() title, _ := rssDoc.ChildrenFiltered("title").Html()
assert.EqualValues(t, "Feed of &#34;the_1-user.with.all.allowedChars&#34;", title) assert.EqualValues(t, "Feed of &#34;the_1-user.with.all.allowedChars&#34;", title)
description, _ := rssDoc.ChildrenFiltered("description").Html() description, _ := rssDoc.ChildrenFiltered("description").Html()
assert.EqualValues(t, "&lt;p&gt;some &lt;a href=&#34;https://commonmark.org/&#34; rel=&#34;nofollow&#34;&gt;commonmark&lt;/a&gt;!&lt;/p&gt;\n", description) assert.EqualValues(t, "&lt;p dir=&#34;auto&#34;&gt;some &lt;a href=&#34;https://commonmark.org/&#34; rel=&#34;nofollow&#34;&gt;commonmark&lt;/a&gt;!&lt;/p&gt;\n", description)
} }
} }

View file

@ -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,

View file

@ -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,