Skip to content

Commit 06f55a9

Browse files
BLumiazeripath
authored and
Sysoev, Vladimir
committed
Support localized README (go-gitea#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 <[email protected]> * Update modules/markup/renderer.go * Update modules/markup/renderer.go * Update modules/markup/renderer.go Co-authored-by: Andrew Thornton <[email protected]>
1 parent cd1f661 commit 06f55a9

File tree

4 files changed

+182
-92
lines changed

4 files changed

+182
-92
lines changed

modules/markup/renderer.go

+26-7
Original file line numberDiff line numberDiff line change
@@ -310,18 +310,37 @@ func IsMarkupFile(name, markup string) bool {
310310
}
311311

312312
// IsReadmeFile reports whether name looks like a README file
313-
// based on its name. If an extension is provided, it will strictly
314-
// match that extension.
315-
// Note that the '.' should be provided in ext, e.g ".md"
316-
func IsReadmeFile(name string, ext ...string) bool {
313+
// based on its name.
314+
func IsReadmeFile(name string) bool {
317315
name = strings.ToLower(name)
318-
if len(ext) > 0 {
319-
return name == "readme"+ext[0]
320-
}
321316
if len(name) < 6 {
322317
return false
323318
} else if len(name) == 6 {
324319
return name == "readme"
325320
}
326321
return name[:7] == "readme."
327322
}
323+
324+
// IsReadmeFileExtension reports whether name looks like a README file
325+
// based on its name. It will look through the provided extensions and check if the file matches
326+
// one of the extensions and provide the index in the extension list.
327+
// If the filename is `readme.` with an unmatched extension it will match with the index equaling
328+
// the length of the provided extension list.
329+
// Note that the '.' should be provided in ext, e.g ".md"
330+
func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
331+
if len(name) < 6 || name[:6] != "readme" {
332+
return 0, false
333+
}
334+
335+
for i, extension := range ext {
336+
if name[6:] == extension {
337+
return i, true
338+
}
339+
}
340+
341+
if name[6] == '.' {
342+
return len(ext), true
343+
}
344+
345+
return 0, false
346+
}

modules/markup/renderer_test.go

+39-16
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,47 @@ func TestMisc_IsReadmeFile(t *testing.T) {
4040
assert.False(t, IsReadmeFile(testCase))
4141
}
4242

43-
trueTestCasesStrict := [][]string{
44-
{"readme", ""},
45-
{"readme.md", ".md"},
46-
{"readme.txt", ".txt"},
47-
}
48-
falseTestCasesStrict := [][]string{
49-
{"readme", ".md"},
50-
{"readme.md", ""},
51-
{"readme.md", ".txt"},
52-
{"readme.md", "md"},
53-
{"readmee.md", ".md"},
54-
{"readme.i18n.md", ".md"},
43+
type extensionTestcase struct {
44+
name string
45+
expected bool
46+
idx int
5547
}
5648

57-
for _, testCase := range trueTestCasesStrict {
58-
assert.True(t, IsReadmeFile(testCase[0], testCase[1]))
49+
exts := []string{".md", ".txt", ""}
50+
testCasesExtensions := []extensionTestcase{
51+
{
52+
name: "readme",
53+
expected: true,
54+
idx: 2,
55+
},
56+
{
57+
name: "readme.md",
58+
expected: true,
59+
idx: 0,
60+
},
61+
{
62+
name: "readme.txt",
63+
expected: true,
64+
idx: 1,
65+
},
66+
{
67+
name: "readme.doc",
68+
expected: true,
69+
idx: 3,
70+
},
71+
{
72+
name: "readmee.md",
73+
},
74+
{
75+
name: "readme..",
76+
expected: true,
77+
idx: 3,
78+
},
5979
}
60-
for _, testCase := range falseTestCasesStrict {
61-
assert.False(t, IsReadmeFile(testCase[0], testCase[1]))
80+
81+
for _, testCase := range testCasesExtensions {
82+
idx, ok := IsReadmeFileExtension(testCase.name, exts...)
83+
assert.Equal(t, testCase.expected, ok)
84+
assert.Equal(t, testCase.idx, idx)
6285
}
6386
}

routers/web/repo/view.go

+54-69
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ type namedBlob struct {
5757
}
5858

5959
// FIXME: There has to be a more efficient way of doing this
60-
func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, error) {
60+
func getReadmeFileFromPath(ctx *context.Context, commit *git.Commit, treePath string) (*namedBlob, error) {
6161
tree, err := commit.SubTree(treePath)
6262
if err != nil {
6363
return nil, err
@@ -68,50 +68,33 @@ func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, err
6868
return nil, err
6969
}
7070

71-
var readmeFiles [4]*namedBlob
72-
exts := []string{".md", ".txt", ""} // sorted by priority
71+
// Create a list of extensions in priority order
72+
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
73+
// 2. Txt files - e.g. README.txt
74+
// 3. No extension - e.g. README
75+
exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority
76+
extCount := len(exts)
77+
readmeFiles := make([]*namedBlob, extCount+1)
7378
for _, entry := range entries {
7479
if entry.IsDir() {
7580
continue
7681
}
77-
for i, ext := range exts {
78-
if markup.IsReadmeFile(entry.Name(), ext) {
79-
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) {
80-
name := entry.Name()
81-
isSymlink := entry.IsLink()
82-
target := entry
83-
if isSymlink {
84-
target, err = entry.FollowLinks()
85-
if err != nil && !git.IsErrBadLink(err) {
86-
return nil, err
87-
}
88-
}
89-
if target != nil && (target.IsExecutable() || target.IsRegular()) {
90-
readmeFiles[i] = &namedBlob{
91-
name,
92-
isSymlink,
93-
target.Blob(),
94-
}
95-
}
96-
}
97-
}
98-
}
99-
100-
if markup.IsReadmeFile(entry.Name()) {
101-
if readmeFiles[3] == nil || base.NaturalSortLess(readmeFiles[3].name, entry.Blob().Name()) {
82+
if i, ok := markup.IsReadmeFileExtension(entry.Name(), exts...); ok {
83+
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) {
10284
name := entry.Name()
10385
isSymlink := entry.IsLink()
86+
target := entry
10487
if isSymlink {
105-
entry, err = entry.FollowLinks()
88+
target, err = entry.FollowLinks()
10689
if err != nil && !git.IsErrBadLink(err) {
10790
return nil, err
10891
}
10992
}
110-
if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
111-
readmeFiles[3] = &namedBlob{
93+
if target != nil && (target.IsExecutable() || target.IsRegular()) {
94+
readmeFiles[i] = &namedBlob{
11295
name,
11396
isSymlink,
114-
entry.Blob(),
97+
target.Blob(),
11598
}
11699
}
117100
}
@@ -151,13 +134,38 @@ func renderDirectory(ctx *context.Context, treeLink string) {
151134
renderReadmeFile(ctx, readmeFile, readmeTreelink)
152135
}
153136

137+
// localizedExtensions prepends the provided language code with and without a
138+
// regional identifier to the provided extenstion.
139+
// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
140+
// Note: ext should be prefixed with a `.`
141+
func localizedExtensions(ext, languageCode string) (localizedExts []string) {
142+
if len(languageCode) < 1 {
143+
return []string{ext}
144+
}
145+
146+
lowerLangCode := "." + strings.ToLower(languageCode)
147+
148+
if strings.Contains(lowerLangCode, "-") {
149+
underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
150+
indexOfDash := strings.Index(lowerLangCode, "-")
151+
// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, .md]
152+
return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, ext}
153+
}
154+
155+
// e.g. [.en.md, .md]
156+
return []string{lowerLangCode + ext, ext}
157+
}
158+
154159
func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string) (*namedBlob, string) {
155-
// 3 for the extensions in exts[] in order
156-
// the last one is for a readme that doesn't
157-
// strictly match an extension
158-
var readmeFiles [4]*namedBlob
159-
var docsEntries [3]*git.TreeEntry
160-
exts := []string{".md", ".txt", ""} // sorted by priority
160+
// Create a list of extensions in priority order
161+
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
162+
// 2. Txt files - e.g. README.txt
163+
// 3. No extension - e.g. README
164+
exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority
165+
extCount := len(exts)
166+
readmeFiles := make([]*namedBlob, extCount+1)
167+
168+
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
161169
for _, entry := range entries {
162170
if entry.IsDir() {
163171
lowerName := strings.ToLower(entry.Name())
@@ -178,47 +186,24 @@ func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string)
178186
continue
179187
}
180188

181-
for i, ext := range exts {
182-
if markup.IsReadmeFile(entry.Name(), ext) {
183-
log.Debug("%s", entry.Name())
184-
name := entry.Name()
185-
isSymlink := entry.IsLink()
186-
target := entry
187-
if isSymlink {
188-
var err error
189-
target, err = entry.FollowLinks()
190-
if err != nil && !git.IsErrBadLink(err) {
191-
ctx.ServerError("FollowLinks", err)
192-
return nil, ""
193-
}
194-
}
195-
log.Debug("%t", target == nil)
196-
if target != nil && (target.IsExecutable() || target.IsRegular()) {
197-
readmeFiles[i] = &namedBlob{
198-
name,
199-
isSymlink,
200-
target.Blob(),
201-
}
202-
}
203-
}
204-
}
205-
206-
if markup.IsReadmeFile(entry.Name()) {
189+
if i, ok := markup.IsReadmeFileExtension(entry.Name(), exts...); ok {
190+
log.Debug("Potential readme file: %s", entry.Name())
207191
name := entry.Name()
208192
isSymlink := entry.IsLink()
193+
target := entry
209194
if isSymlink {
210195
var err error
211-
entry, err = entry.FollowLinks()
196+
target, err = entry.FollowLinks()
212197
if err != nil && !git.IsErrBadLink(err) {
213198
ctx.ServerError("FollowLinks", err)
214199
return nil, ""
215200
}
216201
}
217-
if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
218-
readmeFiles[3] = &namedBlob{
202+
if target != nil && (target.IsExecutable() || target.IsRegular()) {
203+
readmeFiles[i] = &namedBlob{
219204
name,
220205
isSymlink,
221-
entry.Blob(),
206+
target.Blob(),
222207
}
223208
}
224209
}
@@ -239,7 +224,7 @@ func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string)
239224
continue
240225
}
241226
var err error
242-
readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName())
227+
readmeFile, err = getReadmeFileFromPath(ctx, ctx.Repo.Commit, entry.GetSubJumpablePathName())
243228
if err != nil {
244229
ctx.ServerError("getReadmeFileFromPath", err)
245230
return nil, ""

routers/web/repo/view_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Copyright 2014 The Gogs Authors. All rights reserved.
3+
// Use of this source code is governed by a MIT-style
4+
// license that can be found in the LICENSE file.
5+
6+
package repo
7+
8+
import (
9+
"reflect"
10+
"testing"
11+
)
12+
13+
func Test_localizedExtensions(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
ext string
17+
languageCode string
18+
wantLocalizedExts []string
19+
}{
20+
{
21+
name: "empty language",
22+
ext: ".md",
23+
wantLocalizedExts: []string{".md"},
24+
},
25+
{
26+
name: "No region - lowercase",
27+
languageCode: "en",
28+
ext: ".csv",
29+
wantLocalizedExts: []string{".en.csv", ".csv"},
30+
},
31+
{
32+
name: "No region - uppercase",
33+
languageCode: "FR",
34+
ext: ".txt",
35+
wantLocalizedExts: []string{".fr.txt", ".txt"},
36+
},
37+
{
38+
name: "With region - lowercase",
39+
languageCode: "en-us",
40+
ext: ".md",
41+
wantLocalizedExts: []string{".en-us.md", ".en_us.md", ".en.md", ".md"},
42+
},
43+
{
44+
name: "With region - uppercase",
45+
languageCode: "en-CA",
46+
ext: ".MD",
47+
wantLocalizedExts: []string{".en-ca.MD", ".en_ca.MD", ".en.MD", ".MD"},
48+
},
49+
{
50+
name: "With region - all uppercase",
51+
languageCode: "ZH-TW",
52+
ext: ".md",
53+
wantLocalizedExts: []string{".zh-tw.md", ".zh_tw.md", ".zh.md", ".md"},
54+
},
55+
}
56+
for _, tt := range tests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
if gotLocalizedExts := localizedExtensions(tt.ext, tt.languageCode); !reflect.DeepEqual(gotLocalizedExts, tt.wantLocalizedExts) {
59+
t.Errorf("localizedExtensions() = %v, want %v", gotLocalizedExts, tt.wantLocalizedExts)
60+
}
61+
})
62+
}
63+
}

0 commit comments

Comments
 (0)