mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	Add RTL rendering support to Markdown (#24816)
Support RTL content in Markdown:  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:
		| @@ -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, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user