From 32d9c47ec7706d8f06e09b42e09a28d7a0e3c526 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 20 May 2023 23:02:52 +0200 Subject: [PATCH] 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 --- modules/markup/html.go | 2 +- modules/markup/markdown/goldmark.go | 10 ++++++++++ modules/markup/renderer.go | 6 ++++-- services/markup/processorhelper.go | 1 + tests/integration/user_test.go | 2 +- web_src/css/base.css | 1 + web_src/css/markup/content.css | 6 +++++- 7 files changed, 23 insertions(+), 5 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index 11888b8536..da16bcd3cb 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -630,7 +630,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { } 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")) node = node.NextSibling.NextSibling } else { diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 816e93b700..f03a780900 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -47,6 +47,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa 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]) _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { @@ -69,6 +75,9 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa header.ID = util.BytesToReadOnlyString(id.([]byte)) } tocList = append(tocList, header) + applyElementDir(v) + case *ast.Paragraph: + applyElementDir(v) case *ast.Image: // Images need two things: // @@ -171,6 +180,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa v.AppendChild(v, newChild) } } + applyElementDir(v) case *ast.Text: if v.SoftLineBreak() && !v.HardLineBreak() { renderMetas := pc.Get(renderMetasKey).(map[string]string) diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index f2477f1e9e..0331c3742a 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -30,14 +30,16 @@ const ( type ProcessorHelper struct { 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 func Init(ph *ProcessorHelper) { if ph != nil { - processorHelper = *ph + DefaultProcessorHelper = *ph } NewSanitizer() diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index 2897f203a9..3551f85c46 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -13,6 +13,7 @@ import ( func ProcessorHelper() *markup.ProcessorHelper { return &markup.ProcessorHelper{ + ElementDir: "auto", // set dir="auto" for necessary (eg:

, , etc) tags IsUsernameMentionable: func(ctx context.Context, username string) bool { mentionedUser, err := user.GetUserByName(ctx, username) if err != nil { diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index fa8e6e85c7..65cba1dee3 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -250,7 +250,7 @@ func TestGetUserRss(t *testing.T) { title, _ := rssDoc.ChildrenFiltered("title").Html() assert.EqualValues(t, "Feed of "the_1-user.with.all.allowedChars"", title) 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) } } diff --git a/web_src/css/base.css b/web_src/css/base.css index 36624ab957..6c6c5381ad 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1091,6 +1091,7 @@ a.label, color: var(--color-text); background: var(--color-box-body); 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, diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index db67ac4263..5b2d6ef244 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -23,9 +23,9 @@ } .markup .anchor { + float: left; padding-right: 4px; margin-left: -20px; - line-height: 1; color: inherit; } @@ -37,6 +37,10 @@ outline: none; } +.markup h1 .anchor { + margin-top: -2px; /* re-align to center */ +} + .markup h1 .anchor .svg, .markup h2 .anchor .svg, .markup h3 .anchor .svg,