mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-27 00:23:41 +09:00 
			
		
		
		
	Support localized README (#20508)
* Support localized README * Slightly simplify getting the readme file and add some tests. Ensure that i18n also works for docs/ etc. Signed-off-by: Andrew Thornton <art27@cantab.net> * Update modules/markup/renderer.go * Update modules/markup/renderer.go * Update modules/markup/renderer.go Co-authored-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
		| @@ -310,14 +310,9 @@ func IsMarkupFile(name, markup string) bool { | |||||||
| } | } | ||||||
|  |  | ||||||
| // IsReadmeFile reports whether name looks like a README file | // IsReadmeFile reports whether name looks like a README file | ||||||
| // based on its name. If an extension is provided, it will strictly | // based on its name. | ||||||
| // match that extension. | func IsReadmeFile(name string) bool { | ||||||
| // Note that the '.' should be provided in ext, e.g ".md" |  | ||||||
| func IsReadmeFile(name string, ext ...string) bool { |  | ||||||
| 	name = strings.ToLower(name) | 	name = strings.ToLower(name) | ||||||
| 	if len(ext) > 0 { |  | ||||||
| 		return name == "readme"+ext[0] |  | ||||||
| 	} |  | ||||||
| 	if len(name) < 6 { | 	if len(name) < 6 { | ||||||
| 		return false | 		return false | ||||||
| 	} else if len(name) == 6 { | 	} else if len(name) == 6 { | ||||||
| @@ -325,3 +320,27 @@ func IsReadmeFile(name string, ext ...string) bool { | |||||||
| 	} | 	} | ||||||
| 	return name[:7] == "readme." | 	return name[:7] == "readme." | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // IsReadmeFileExtension reports whether name looks like a README file | ||||||
|  | // based on its name. It will look through the provided extensions and check if the file matches | ||||||
|  | // one of the extensions and provide the index in the extension list. | ||||||
|  | // If the filename is `readme.` with an unmatched extension it will match with the index equaling | ||||||
|  | // the length of the provided extension list. | ||||||
|  | // Note that the '.' should be provided in ext, e.g ".md" | ||||||
|  | func IsReadmeFileExtension(name string, ext ...string) (int, bool) { | ||||||
|  | 	if len(name) < 6 || name[:6] != "readme" { | ||||||
|  | 		return 0, false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i, extension := range ext { | ||||||
|  | 		if name[6:] == extension { | ||||||
|  | 			return i, true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if name[6] == '.' { | ||||||
|  | 		return len(ext), true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return 0, false | ||||||
|  | } | ||||||
|   | |||||||
| @@ -40,24 +40,47 @@ func TestMisc_IsReadmeFile(t *testing.T) { | |||||||
| 		assert.False(t, IsReadmeFile(testCase)) | 		assert.False(t, IsReadmeFile(testCase)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	trueTestCasesStrict := [][]string{ | 	type extensionTestcase struct { | ||||||
| 		{"readme", ""}, | 		name     string | ||||||
| 		{"readme.md", ".md"}, | 		expected bool | ||||||
| 		{"readme.txt", ".txt"}, | 		idx      int | ||||||
| 	} |  | ||||||
| 	falseTestCasesStrict := [][]string{ |  | ||||||
| 		{"readme", ".md"}, |  | ||||||
| 		{"readme.md", ""}, |  | ||||||
| 		{"readme.md", ".txt"}, |  | ||||||
| 		{"readme.md", "md"}, |  | ||||||
| 		{"readmee.md", ".md"}, |  | ||||||
| 		{"readme.i18n.md", ".md"}, |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, testCase := range trueTestCasesStrict { | 	exts := []string{".md", ".txt", ""} | ||||||
| 		assert.True(t, IsReadmeFile(testCase[0], testCase[1])) | 	testCasesExtensions := []extensionTestcase{ | ||||||
|  | 		{ | ||||||
|  | 			name:     "readme", | ||||||
|  | 			expected: true, | ||||||
|  | 			idx:      2, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:     "readme.md", | ||||||
|  | 			expected: true, | ||||||
|  | 			idx:      0, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:     "readme.txt", | ||||||
|  | 			expected: true, | ||||||
|  | 			idx:      1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:     "readme.doc", | ||||||
|  | 			expected: true, | ||||||
|  | 			idx:      3, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "readmee.md", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:     "readme..", | ||||||
|  | 			expected: true, | ||||||
|  | 			idx:      3, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 	for _, testCase := range falseTestCasesStrict { |  | ||||||
| 		assert.False(t, IsReadmeFile(testCase[0], testCase[1])) | 	for _, testCase := range testCasesExtensions { | ||||||
|  | 		idx, ok := IsReadmeFileExtension(testCase.name, exts...) | ||||||
|  | 		assert.Equal(t, testCase.expected, ok) | ||||||
|  | 		assert.Equal(t, testCase.idx, idx) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ type namedBlob struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| // FIXME: There has to be a more efficient way of doing this | // FIXME: There has to be a more efficient way of doing this | ||||||
| func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, error) { | func getReadmeFileFromPath(ctx *context.Context, commit *git.Commit, treePath string) (*namedBlob, error) { | ||||||
| 	tree, err := commit.SubTree(treePath) | 	tree, err := commit.SubTree(treePath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -68,50 +68,33 @@ func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, err | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var readmeFiles [4]*namedBlob | 	// Create a list of extensions in priority order | ||||||
| 	exts := []string{".md", ".txt", ""} // sorted by priority | 	// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md | ||||||
|  | 	// 2. Txt files - e.g. README.txt | ||||||
|  | 	// 3. No extension - e.g. README | ||||||
|  | 	exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority | ||||||
|  | 	extCount := len(exts) | ||||||
|  | 	readmeFiles := make([]*namedBlob, extCount+1) | ||||||
| 	for _, entry := range entries { | 	for _, entry := range entries { | ||||||
| 		if entry.IsDir() { | 		if entry.IsDir() { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		for i, ext := range exts { | 		if i, ok := markup.IsReadmeFileExtension(entry.Name(), exts...); ok { | ||||||
| 			if markup.IsReadmeFile(entry.Name(), ext) { | 			if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) { | ||||||
| 				if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) { |  | ||||||
| 					name := entry.Name() |  | ||||||
| 					isSymlink := entry.IsLink() |  | ||||||
| 					target := entry |  | ||||||
| 					if isSymlink { |  | ||||||
| 						target, err = entry.FollowLinks() |  | ||||||
| 						if err != nil && !git.IsErrBadLink(err) { |  | ||||||
| 							return nil, err |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 					if target != nil && (target.IsExecutable() || target.IsRegular()) { |  | ||||||
| 						readmeFiles[i] = &namedBlob{ |  | ||||||
| 							name, |  | ||||||
| 							isSymlink, |  | ||||||
| 							target.Blob(), |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if markup.IsReadmeFile(entry.Name()) { |  | ||||||
| 			if readmeFiles[3] == nil || base.NaturalSortLess(readmeFiles[3].name, entry.Blob().Name()) { |  | ||||||
| 				name := entry.Name() | 				name := entry.Name() | ||||||
| 				isSymlink := entry.IsLink() | 				isSymlink := entry.IsLink() | ||||||
|  | 				target := entry | ||||||
| 				if isSymlink { | 				if isSymlink { | ||||||
| 					entry, err = entry.FollowLinks() | 					target, err = entry.FollowLinks() | ||||||
| 					if err != nil && !git.IsErrBadLink(err) { | 					if err != nil && !git.IsErrBadLink(err) { | ||||||
| 						return nil, err | 						return nil, err | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 				if entry != nil && (entry.IsExecutable() || entry.IsRegular()) { | 				if target != nil && (target.IsExecutable() || target.IsRegular()) { | ||||||
| 					readmeFiles[3] = &namedBlob{ | 					readmeFiles[i] = &namedBlob{ | ||||||
| 						name, | 						name, | ||||||
| 						isSymlink, | 						isSymlink, | ||||||
| 						entry.Blob(), | 						target.Blob(), | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @@ -151,13 +134,38 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||||||
| 	renderReadmeFile(ctx, readmeFile, readmeTreelink) | 	renderReadmeFile(ctx, readmeFile, readmeTreelink) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // localizedExtensions prepends the provided language code with and without a | ||||||
|  | // regional identifier to the provided extenstion. | ||||||
|  | // Note: the language code will always be lower-cased, if a region is present it must be separated with a `-` | ||||||
|  | // Note: ext should be prefixed with a `.` | ||||||
|  | func localizedExtensions(ext, languageCode string) (localizedExts []string) { | ||||||
|  | 	if len(languageCode) < 1 { | ||||||
|  | 		return []string{ext} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	lowerLangCode := "." + strings.ToLower(languageCode) | ||||||
|  |  | ||||||
|  | 	if strings.Contains(lowerLangCode, "-") { | ||||||
|  | 		underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_") | ||||||
|  | 		indexOfDash := strings.Index(lowerLangCode, "-") | ||||||
|  | 		// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, .md] | ||||||
|  | 		return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, ext} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// e.g. [.en.md, .md] | ||||||
|  | 	return []string{lowerLangCode + ext, ext} | ||||||
|  | } | ||||||
|  |  | ||||||
| func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string) (*namedBlob, string) { | func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string) (*namedBlob, string) { | ||||||
| 	// 3 for the extensions in exts[] in order | 	// Create a list of extensions in priority order | ||||||
| 	// the last one is for a readme that doesn't | 	// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md | ||||||
| 	// strictly match an extension | 	// 2. Txt files - e.g. README.txt | ||||||
| 	var readmeFiles [4]*namedBlob | 	// 3. No extension - e.g. README | ||||||
| 	var docsEntries [3]*git.TreeEntry | 	exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority | ||||||
| 	exts := []string{".md", ".txt", ""} // sorted by priority | 	extCount := len(exts) | ||||||
|  | 	readmeFiles := make([]*namedBlob, extCount+1) | ||||||
|  |  | ||||||
|  | 	docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/) | ||||||
| 	for _, entry := range entries { | 	for _, entry := range entries { | ||||||
| 		if entry.IsDir() { | 		if entry.IsDir() { | ||||||
| 			lowerName := strings.ToLower(entry.Name()) | 			lowerName := strings.ToLower(entry.Name()) | ||||||
| @@ -178,47 +186,24 @@ func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string) | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		for i, ext := range exts { | 		if i, ok := markup.IsReadmeFileExtension(entry.Name(), exts...); ok { | ||||||
| 			if markup.IsReadmeFile(entry.Name(), ext) { | 			log.Debug("Potential readme file: %s", entry.Name()) | ||||||
| 				log.Debug("%s", entry.Name()) |  | ||||||
| 				name := entry.Name() |  | ||||||
| 				isSymlink := entry.IsLink() |  | ||||||
| 				target := entry |  | ||||||
| 				if isSymlink { |  | ||||||
| 					var err error |  | ||||||
| 					target, err = entry.FollowLinks() |  | ||||||
| 					if err != nil && !git.IsErrBadLink(err) { |  | ||||||
| 						ctx.ServerError("FollowLinks", err) |  | ||||||
| 						return nil, "" |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 				log.Debug("%t", target == nil) |  | ||||||
| 				if target != nil && (target.IsExecutable() || target.IsRegular()) { |  | ||||||
| 					readmeFiles[i] = &namedBlob{ |  | ||||||
| 						name, |  | ||||||
| 						isSymlink, |  | ||||||
| 						target.Blob(), |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if markup.IsReadmeFile(entry.Name()) { |  | ||||||
| 			name := entry.Name() | 			name := entry.Name() | ||||||
| 			isSymlink := entry.IsLink() | 			isSymlink := entry.IsLink() | ||||||
|  | 			target := entry | ||||||
| 			if isSymlink { | 			if isSymlink { | ||||||
| 				var err error | 				var err error | ||||||
| 				entry, err = entry.FollowLinks() | 				target, err = entry.FollowLinks() | ||||||
| 				if err != nil && !git.IsErrBadLink(err) { | 				if err != nil && !git.IsErrBadLink(err) { | ||||||
| 					ctx.ServerError("FollowLinks", err) | 					ctx.ServerError("FollowLinks", err) | ||||||
| 					return nil, "" | 					return nil, "" | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			if entry != nil && (entry.IsExecutable() || entry.IsRegular()) { | 			if target != nil && (target.IsExecutable() || target.IsRegular()) { | ||||||
| 				readmeFiles[3] = &namedBlob{ | 				readmeFiles[i] = &namedBlob{ | ||||||
| 					name, | 					name, | ||||||
| 					isSymlink, | 					isSymlink, | ||||||
| 					entry.Blob(), | 					target.Blob(), | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -239,7 +224,7 @@ func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string) | |||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 			var err error | 			var err error | ||||||
| 			readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName()) | 			readmeFile, err = getReadmeFileFromPath(ctx, ctx.Repo.Commit, entry.GetSubJumpablePathName()) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				ctx.ServerError("getReadmeFileFromPath", err) | 				ctx.ServerError("getReadmeFileFromPath", err) | ||||||
| 				return nil, "" | 				return nil, "" | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								routers/web/repo/view_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								routers/web/repo/view_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | // Copyright 2017 The Gitea Authors. All rights reserved. | ||||||
|  | // Copyright 2014 The Gogs Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package repo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func Test_localizedExtensions(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name              string | ||||||
|  | 		ext               string | ||||||
|  | 		languageCode      string | ||||||
|  | 		wantLocalizedExts []string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:              "empty language", | ||||||
|  | 			ext:               ".md", | ||||||
|  | 			wantLocalizedExts: []string{".md"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:              "No region - lowercase", | ||||||
|  | 			languageCode:      "en", | ||||||
|  | 			ext:               ".csv", | ||||||
|  | 			wantLocalizedExts: []string{".en.csv", ".csv"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:              "No region - uppercase", | ||||||
|  | 			languageCode:      "FR", | ||||||
|  | 			ext:               ".txt", | ||||||
|  | 			wantLocalizedExts: []string{".fr.txt", ".txt"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:              "With region - lowercase", | ||||||
|  | 			languageCode:      "en-us", | ||||||
|  | 			ext:               ".md", | ||||||
|  | 			wantLocalizedExts: []string{".en-us.md", ".en_us.md", ".en.md", ".md"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:              "With region - uppercase", | ||||||
|  | 			languageCode:      "en-CA", | ||||||
|  | 			ext:               ".MD", | ||||||
|  | 			wantLocalizedExts: []string{".en-ca.MD", ".en_ca.MD", ".en.MD", ".MD"}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:              "With region - all uppercase", | ||||||
|  | 			languageCode:      "ZH-TW", | ||||||
|  | 			ext:               ".md", | ||||||
|  | 			wantLocalizedExts: []string{".zh-tw.md", ".zh_tw.md", ".zh.md", ".md"}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			if gotLocalizedExts := localizedExtensions(tt.ext, tt.languageCode); !reflect.DeepEqual(gotLocalizedExts, tt.wantLocalizedExts) { | ||||||
|  | 				t.Errorf("localizedExtensions() = %v, want %v", gotLocalizedExts, tt.wantLocalizedExts) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user