diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index a674984a2584e..79f41f9b1bb56 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -145,6 +145,17 @@ CUSTOM_URL_SCHEMES = ; List of file extensions that should be rendered/edited as Markdown ; Separate the extensions with a comma. To render files without any extension as markdown, just put a comma FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd +; Create 'Table of Pages' on internal wiki pages +DEFAULT_TOC_WIKI_TREE = true +; Create Table of Contents based on headlines on wiki page (does not alter file) +DEFAULT_TOC_WIKI_FILE = true +; Create 'Table of Contents' on all rendered markup files. This does not alter files. Enabled: overwrites DEFAULT_TOC_MARKUP_BY_FLAG +DEFAULT_TOC_MARKUP_ALWAYS = false +; Create 'Table of Contents' on rendered markup files if line '%%TOC%%' is present. +DEFAULT_TOC_MARKUP_BY_FLAG = true +; List of markup file extensions that TOC should be created on, set to not interfere with external markup renderer. +; Separate the extensions with a comma. To ignore file extension check, just put a comma. +TOC_MARKUP_FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd,.org [server] ; The protocol the server listens on. One of 'http', 'https', 'unix' or 'fcgi'. diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 11dbfc5d095ec..8ee9663c7cbaf 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -107,6 +107,13 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. ## Markdown (`markdown`) - `ENABLE_HARD_LINE_BREAK`: **false**: Enable Markdown's hard line break extension. +- `DEFAULT_TOC_WIKI_TREE`: **true**: Create 'Table of Pages' on internal wiki pages. +- `DEFAULT_TOC_WIKI_FILE`: **true**: Create Table of Contents based on headlines on wiki page (does + not alter file) +- `DEFAULT_TOC_MARKUP_ALWAYS`: **false** : Create 'Table of Contents' on all rendered markup files. This does not alter files. Enabled: overwrites `DEFAULT_TOC_MARKUP_BY_FLAG` +- `DEFAULT_TOC_MARKUP_BY_FLAG`: **true** : Create 'Table of Contents' on rendered markup files if line `%%TOC%%` is present. +- `TOC_MARKUP_FILE_EXTENSIONS`: **.md,.markdown,.mdown,.mkd,.org** : List of markup file extensions that TOC should be created on, set to not interfere with external markup renderer. + Separate the extensions with a comma. To ignore file extension check, just put a comma. ## Server (`server`) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index ef43b0453b092..1fefa176b9c02 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -232,6 +232,10 @@ var migrations = []Migration{ NewMigration("add avatar field to repository", addAvatarFieldToRepository), // v88 -> v89 NewMigration("add commit status context field to commit_status", addCommitStatusContext), + // v89 -> v90 + NewMigration("add toc on wiki and markedown", addCanTocOnWikiAndMarkdown), + // v90 -> v91 + NewMigration("add pagetoc to wiki", addCanWikiPageToc), } // Migrate database to current version diff --git a/models/migrations/v89.go b/models/migrations/v89.go new file mode 100644 index 0000000000000..52295efeb8979 --- /dev/null +++ b/models/migrations/v89.go @@ -0,0 +1,40 @@ +// Copyright 2019 The Gitea 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 migrations + +import ( + "code.gitea.io/gitea/modules/setting" + + "github.com/go-xorm/xorm" +) + +func addCanTocOnWikiAndMarkdown(x *xorm.Engine) error { + + type Repository struct { + TocWikiFile bool `xorm:"NOT NULL DEFAULT true"` + TocMarkupAlways bool `xorm:"NOT NULL DEFAULT false"` + TocMarkupByFlag bool `xorm:"NOT NULL DEFAULT true"` + } + + if err := x.Sync2(new(Repository)); err != nil { + return err + } + + if _, err := x.Exec("UPDATE repository SET toc_wiki_file = ?", + setting.Markdown.DefaultTocWikiFile); err != nil { + return err + } + + if _, err := x.Exec("UPDATE repository SET toc_markup_always = ?", + setting.Markdown.DefaultTocMarkupAlways); err != nil { + return err + } + + if _, err := x.Exec("UPDATE repository SET toc_markup_by_flag = ?", + setting.Markdown.DefaultTocMarkupByFlag); err != nil { + return err + } + return nil +} diff --git a/models/migrations/v90.go b/models/migrations/v90.go new file mode 100644 index 0000000000000..7879c8527b17f --- /dev/null +++ b/models/migrations/v90.go @@ -0,0 +1,28 @@ +// Copyright 2019 The Gitea 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 migrations + +import ( + "code.gitea.io/gitea/modules/setting" + + "github.com/go-xorm/xorm" +) + +func addCanWikiPageToc(x *xorm.Engine) error { + + type Repository struct { + TocWikiTree bool `xorm:"NOT NULL DEFAULT true"` + } + + if err := x.Sync2(new(Repository)); err != nil { + return err + } + + if _, err := x.Exec("UPDATE repository SET toc_wiki_tree = ?", + setting.Markdown.DefaultTocWikiTree); err != nil { + return err + } + return nil +} diff --git a/models/repo.go b/models/repo.go index 59ce18fa88e9e..47e343b7b8253 100644 --- a/models/repo.go +++ b/models/repo.go @@ -169,6 +169,10 @@ type Repository struct { IndexerStatus *RepoIndexerStatus `xorm:"-"` IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` + TocWikiTree bool `xorm:"NOT NULL DEFAULT true"` + TocWikiFile bool `xorm:"NOT NULL DEFAULT true"` + TocMarkupAlways bool `xorm:"NOT NULL DEFAULT false"` + TocMarkupByFlag bool `xorm:"NOT NULL DEFAULT true"` Topics []string `xorm:"TEXT JSON"` // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols @@ -1361,6 +1365,10 @@ func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err IsPrivate: opts.IsPrivate, IsFsckEnabled: !opts.IsMirror, CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, + TocWikiTree: setting.Markdown.DefaultTocWikiTree, + TocWikiFile: setting.Markdown.DefaultTocWikiFile, + TocMarkupAlways: setting.Markdown.DefaultTocMarkupAlways, + TocMarkupByFlag: setting.Markdown.DefaultTocMarkupByFlag, } sess := x.NewSession() diff --git a/models/wiki.go b/models/wiki.go index 9ae3386333298..800bea8b73c72 100644 --- a/models/wiki.go +++ b/models/wiki.go @@ -9,17 +9,19 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strings" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/sync" + "code.gitea.io/gitea/modules/util" "github.com/Unknwon/com" ) var ( - reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"} + reservedWikiNames = []string{"_pages", "_new", "_edit", "_delete", "raw"} wikiWorkingPool = sync.NewExclusivePool() ) @@ -28,9 +30,17 @@ func NormalizeWikiName(name string) string { return strings.Replace(name, "-", " ", -1) } -// WikiNameToSubURL converts a wiki name to its corresponding sub-URL. +// WikiNameToSubURL converts a wiki name to its corresponding sub-URL. This will escape dangerous letters. func WikiNameToSubURL(name string) string { - return url.QueryEscape(strings.Replace(name, " ", "-", -1)) + // remove path up + re1 := regexp.MustCompile(`(\.\.\/)`) + name = re1.ReplaceAllString(name, "") + // trim whitespace and / + name = strings.Trim(name, "\n\r\t /") + name = url.QueryEscape(name) + //restore spaces + re3 := regexp.MustCompile(`(?m)(%20|\+)`) + return re3.ReplaceAllString(name, "%20") } // WikiNameToFilename converts a wiki name to its corresponding filename. @@ -39,17 +49,53 @@ func WikiNameToFilename(name string) string { return url.QueryEscape(name) + ".md" } +// WikiNameToPathFilename converts a wiki name to its corresponding filename, keep directory paths. +func WikiNameToPathFilename(name string) string { + var restore = [1][2]string{ + {`(\.\.\/)`, ""}, // remove path up + } + for _, kv := range restore { + loopRe := regexp.MustCompile(kv[0]) + name = loopRe.ReplaceAllString(name, kv[1]) + } + name = strings.Trim(name, "\n\r\t ./") // trim whitespace and / . + return name + ".md" +} + +// FilenameToPathFilename converts a wiki filename to filename with filepath. +func FilenameToPathFilename(name string) string { + // restore spaces and slashes + var restore = [4][2]string{ + {`(?m)%2F`, "/"}, //recover slashes / + {`(?m)(%20|\+)`, " "}, //restore spaces + {`(?m)(%25)`, "%"}, //restore % + {`(?m)(%26)`, "&"}, //restore & + } + for _, kv := range restore { + loopRe := regexp.MustCompile(kv[0]) + name = loopRe.ReplaceAllString(name, kv[1]) + } + return name +} + +// WikiNameToRawPrefix Get raw file path inside wiki, removes last path element and returns +func WikiNameToRawPrefix(repositoryName string, wikiPage string) string { + a := strings.Split(wikiPage, "/") + a = a[:len(a)-1] + return util.URLJoin(repositoryName, "wiki", "raw", strings.Join(a, "/")) +} + // WikiFilenameToName converts a wiki filename to its corresponding page name. -func WikiFilenameToName(filename string) (string, error) { +func WikiFilenameToName(filename string) (string, string, error) { if !strings.HasSuffix(filename, ".md") { - return "", ErrWikiInvalidFileName{filename} + return "", "", ErrWikiInvalidFileName{filename} } basename := filename[:len(filename)-3] unescaped, err := url.QueryUnescape(basename) if err != nil { - return "", err + return basename, basename, err } - return NormalizeWikiName(unescaped), nil + return unescaped, basename, nil } // WikiCloneLink returns clone URLs of repository wiki. @@ -149,6 +195,8 @@ func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, con } newWikiPath := WikiNameToFilename(newWikiName) + newWikiDirPath := WikiNameToPathFilename(newWikiName) + if isNew { filesInIndex, err := gitRepo.LsFiles(newWikiPath) if err != nil { @@ -160,6 +208,26 @@ func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, con return ErrWikiAlreadyExist{newWikiPath} } } + filesInIndex, err = gitRepo.LsFiles(newWikiDirPath) + if err != nil { + log.Error("%v", err) + return err + } + for _, file := range filesInIndex { + if file == newWikiDirPath { + return ErrWikiAlreadyExist{newWikiDirPath} + } + } + filesInIndex, err = gitRepo.LsFiles(FilenameToPathFilename(newWikiDirPath)) + if err != nil { + log.Error("%v", err) + return err + } + for _, file := range filesInIndex { + if file == newWikiDirPath { + return ErrWikiAlreadyExist{newWikiDirPath} + } + } } else { oldWikiPath := WikiNameToFilename(oldWikiName) filesInIndex, err := gitRepo.LsFiles(oldWikiPath) @@ -181,8 +249,30 @@ func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, con return err } } + oldWikiDirPath := WikiNameToPathFilename(oldWikiName) + filesInIndex, err = gitRepo.LsFiles(oldWikiDirPath) + if err != nil { + log.Error("%v", err) + return err + } + found = false + for _, file := range filesInIndex { + if file == oldWikiDirPath { + found = true + break + } + } + if found { + err := gitRepo.RemoveFilesFromIndex(oldWikiDirPath) + if err != nil { + log.Error("%v", err) + return err + } + } } + newWikiDirPath = FilenameToPathFilename(newWikiDirPath) + // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here objectHash, err := gitRepo.HashObject(strings.NewReader(content)) @@ -191,7 +281,7 @@ func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, con return err } - if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil { + if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiDirPath); err != nil { log.Error("%v", err) return err } @@ -291,7 +381,23 @@ func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error) return err } } else { - return os.ErrNotExist + wikiDirPath := WikiNameToPathFilename(wikiName) + filesInIndex, err = gitRepo.LsFiles(wikiDirPath) + found = false + for _, file := range filesInIndex { + if file == wikiDirPath { + found = true + break + } + } + if found { + err := gitRepo.RemoveFilesFromIndex(wikiDirPath) + if err != nil { + return err + } + } else { + return os.ErrNotExist + } } // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here diff --git a/models/wiki_test.go b/models/wiki_test.go index 991a3d95b9036..a9e4610de1548 100644 --- a/models/wiki_test.go +++ b/models/wiki_test.go @@ -6,6 +6,7 @@ package models import ( "path/filepath" + "regexp" "testing" "code.gitea.io/gitea/modules/git" @@ -30,6 +31,22 @@ func TestNormalizeWikiName(t *testing.T) { } } +func TestWikiNameToSubURL(t *testing.T) { + type test struct { + Expected string + WikiName string + } + for _, test := range []test{ + {"wiki%2Fpath", "wiki/../path/../../"}, + {"wiki%2Fpath", " wiki/path ////// "}, + {"wiki-name", "wiki-name"}, + {"name%20with%2Fslash", "name with/slash"}, + {"name%20with%25percent", "name with%percent"}, + } { + assert.Equal(t, test.Expected, WikiNameToSubURL(test.WikiName)) + } +} + func TestWikiNameToFilename(t *testing.T) { type test struct { Expected string @@ -40,48 +57,88 @@ func TestWikiNameToFilename(t *testing.T) { {"wiki-name.md", "wiki-name"}, {"name-with%2Fslash.md", "name with/slash"}, {"name-with%25percent.md", "name with%percent"}, + {"wiki-name-with%2Fslash.md", "wiki name with/slash"}, + {"%24%24%24%25%25%25%5E%5E%26%26%21%40%23%24%28%29%2C.%3C%3E.md", "$$$%%%^^&&!@#$(),.<>"}, } { assert.Equal(t, test.Expected, WikiNameToFilename(test.WikiName)) } } -func TestWikiNameToSubURL(t *testing.T) { +func TestWikiNameToPathFilename(t *testing.T) { type test struct { Expected string WikiName string } for _, test := range []test{ - {"wiki-name", "wiki name"}, - {"wiki-name", "wiki-name"}, - {"name-with%2Fslash", "name with/slash"}, - {"name-with%25percent", "name with%percent"}, + {"wiki name.md", "wiki name"}, + {"wiki-name.md", "wiki-name"}, + {"name with/slash.md", "name with/slash"}, + {"name with/slash.md", "name with/../slash"}, + {"name with%percent.md", "name with%percent"}, + {"git/config.md", ".git/config "}, } { - assert.Equal(t, test.Expected, WikiNameToSubURL(test.WikiName)) + assert.Equal(t, test.Expected, WikiNameToPathFilename(test.WikiName)) } } -func TestWikiFilenameToName(t *testing.T) { +func TestFilenameToPathFilename(t *testing.T) { type test struct { Expected string Filename string } for _, test := range []test{ - {"hello world", "hello-world.md"}, - {"symbols/?*", "symbols%2F%3F%2A.md"}, + {"wiki/name.md", "wiki%2Fname.md"}, + {"wiki name path", "wiki%20name+path"}, + {"name with/slash", "name with/slash"}, + {"name with&and", "name with%2526and"}, + {"name with%percent", "name with%percent"}, + {"&&&&", "%26%26%26%26"}, } { - name, err := WikiFilenameToName(test.Filename) + assert.Equal(t, test.Expected, FilenameToPathFilename(test.Filename)) + } +} + +func TestWikiNameToRawPrefix(t *testing.T) { + type test struct { + RepoName string + WikiPage string + Expected string + } + for _, test := range []test{ + {"/repo1/name", "wiki/path", "/repo1/name/wiki/raw/wiki"}, + {"/repo2/name", "wiki/path/subdir", "/repo2/name/wiki/raw/wiki/path"}, + } { + assert.Equal(t, test.Expected, WikiNameToRawPrefix(test.RepoName, test.WikiPage)) + } +} + +func TestWikiFilenameToName(t *testing.T) { + type test struct { + Expected1 string + Expected2 string + Filename string + } + for _, test := range []test{ + {"hello world", "hello world", "hello world.md"}, + {"hello-world", "hello-world", "hello-world.md"}, + {"symbols/?*", "symbols%2F%3F%2A", "symbols%2F%3F%2A.md"}, + {"wiki-name-with/slash", "wiki-name-with%2Fslash", "wiki-name-with%2Fslash.md"}, + {"$$$%%%^^&&!@#$(),.<>", "%24%24%24%25%25%25%5E%5E%26%26%21%40%23%24%28%29%2C.%3C%3E", "%24%24%24%25%25%25%5E%5E%26%26%21%40%23%24%28%29%2C.%3C%3E.md"}, + } { + unescaped, basename, err := WikiFilenameToName(test.Filename) assert.NoError(t, err) - assert.Equal(t, test.Expected, name) + assert.Equal(t, test.Expected1, unescaped) + assert.Equal(t, test.Expected2, basename) } for _, badFilename := range []string{ "nofileextension", "wrongfileextension.txt", } { - _, err := WikiFilenameToName(badFilename) + _, _, err := WikiFilenameToName(badFilename) assert.Error(t, err) assert.True(t, IsErrWikiInvalidFileName(err)) } - _, err := WikiFilenameToName("badescaping%%.md") + _, _, err := WikiFilenameToName("badescaping%%.md") assert.Error(t, err) assert.False(t, IsErrWikiInvalidFileName(err)) } @@ -96,9 +153,9 @@ func TestWikiNameToFilenameToName(t *testing.T) { "$$$%%%^^&&!@#$(),.<>", } { filename := WikiNameToFilename(name) - resultName, err := WikiFilenameToName(filename) + resultName, _, err := WikiFilenameToName(filename) assert.NoError(t, err) - assert.Equal(t, NormalizeWikiName(name), resultName) + assert.Equal(t, NormalizeWikiName(name), NormalizeWikiName(resultName)) } } @@ -163,10 +220,12 @@ func TestRepository_AddWikiPage(t *testing.T) { assert.NoError(t, err) masterTree, err := gitRepo.GetTree("master") assert.NoError(t, err) - wikiPath := WikiNameToFilename(wikiName) + wikiPath := WikiNameToPathFilename(wikiName) entry, err := masterTree.GetTreeEntryByPath(wikiPath) + re := regexp.MustCompile(`(?m)(.*)(\/)([^\/]*)$`) + assert.NoError(t, err) - assert.Equal(t, wikiPath, entry.Name(), "%s not addded correctly", wikiName) + assert.Equal(t, re.ReplaceAllString(wikiPath, "$3"), entry.Name(), "%s not addded correctly", wikiName) }) } @@ -205,10 +264,13 @@ func TestRepository_EditWikiPage(t *testing.T) { assert.NoError(t, err) masterTree, err := gitRepo.GetTree("master") assert.NoError(t, err) - wikiPath := WikiNameToFilename(newWikiName) + re := regexp.MustCompile(`(?m)(.*)(\/)([^\/]*)$`) + + wikiPath := WikiNameToPathFilename(newWikiName) + entry, err := masterTree.GetTreeEntryByPath(wikiPath) assert.NoError(t, err) - assert.Equal(t, wikiPath, entry.Name(), "%s not editted correctly", newWikiName) + assert.Equal(t, re.ReplaceAllString(wikiPath, "$3"), entry.Name(), "%s not editted correctly", newWikiName) if newWikiName != "Home" { _, err := masterTree.GetTreeEntryByPath("Home.md") diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 0333c3c92614a..1e7ef1c6ac077 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -125,6 +125,10 @@ type RepoSettingForm struct { AllowOnlyContributorsToTrackTime bool EnableIssueDependencies bool IsArchived bool + TocWikiTree bool + TocWikiFile bool + TocMarkupAlways bool + TocMarkupByFlag bool // Admin settings EnableHealthCheck bool diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index aab951c60f8c2..9cd04f35a21a7 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -103,9 +103,6 @@ func (r *Renderer) ListItem(out *bytes.Buffer, text []byte, flags int) { // Image defines how images should be processed to produce corresponding HTML elements. func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { prefix := r.URLPrefix - if r.IsWiki { - prefix = util.URLJoin(prefix, "wiki", "raw") - } prefix = strings.Replace(prefix, "/src/", "/media/", 1) if len(link) > 0 && !markup.IsLink(link) { lnk := string(link) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 43a61aa7faeed..8fbb3e3196b54 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -236,12 +236,22 @@ var ( // Markdown settings Markdown = struct { - EnableHardLineBreak bool - CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` - FileExtensions []string + EnableHardLineBreak bool + CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` + FileExtensions []string + DefaultTocWikiTree bool + DefaultTocWikiFile bool + DefaultTocMarkupAlways bool + DefaultTocMarkupByFlag bool + TocMarkupFileExtensions []string }{ - EnableHardLineBreak: false, - FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","), + EnableHardLineBreak: false, + FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","), + DefaultTocWikiTree: true, + DefaultTocWikiFile: true, + DefaultTocMarkupAlways: false, + DefaultTocMarkupByFlag: true, + TocMarkupFileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.org", ","), } // Admin settings diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 223df91fda469..e13a1f4100449 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1031,6 +1031,7 @@ wiki.default_commit_message = Write a note about this page update (optional). wiki.save_page = Save Page wiki.last_commit_info = %s edited this page %s wiki.edit_page_button = Edit +wiki.abort_edit_page_button = Abort wiki.new_page_button = New Page wiki.delete_page_button = Delete Page wiki.delete_page_notice_1 = Deleting the wiki page '%s' cannot be undone. Continue? @@ -1152,6 +1153,12 @@ settings.pulls.allow_squash_commits = Enable Squashing to Merge Commits settings.admin_settings = Administrator Settings settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) settings.admin_enable_close_issues_via_commit_in_any_branch = Close an issue via a commit made in a non default branch +settings.toc.toc = Table of Contents +settings.toc.pagetoc = Table of Pages +settings.toc.toc_wiki_tree = Enable additional wiki page tree +settings.toc.toc_wiki_file = Enable autogenerated TOC on wikipages (based on headlines) (does not alter files) +settings.toc.toc_markup_always = Enable autogenerated TOC on all markup files (based on headlines) (does not alter files) +settings.toc.toc_markup_by_flag = Enable autogenerated TOC on markup files which contain keyword '%%TOC%%' (based on headlines) (does not alter files) settings.danger_zone = Danger Zone settings.new_owner_has_same_repo = The new owner already has a repository with same name. Please choose another name. settings.convert = Convert to Regular Repository diff --git a/public/css/index.css b/public/css/index.css index 437605e1d31f0..8010267db1faf 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -208,7 +208,7 @@ footer .ui.left,footer .ui.right{line-height:40px} .ui.tabular.menu .item{color:rgba(0,0,0,.5)} .ui.tabular.menu .item:hover{color:rgba(0,0,0,.8)} .ui.tabular.menu .item.active{color:rgba(0,0,0,.9)} -.markdown:not(code){overflow:hidden;font-size:16px;line-height:1.6!important;word-wrap:break-word} +.markdown:not(code){font-size:16px;line-height:1.6!important;word-wrap:break-word} .markdown:not(code).ui.segment{padding:3em} .markdown:not(code).file-view{padding:2em 2em 2em!important} .markdown:not(code)>:first-child{margin-top:0!important} @@ -287,6 +287,28 @@ footer .ui.left,footer .ui.right{line-height:40px} .markdown:not(code) .csv-data tr{border-top:0} .markdown:not(code) .csv-data th{font-weight:700;background:#f8f8f8;border-top:0} .markdown:not(code) .ui.list .list,.markdown:not(code) ol.ui.list ol,.markdown:not(code) ul.ui.list ul{padding-left:2em} +.markdown:not(code) .auto-toc-wrapper{position:relative;padding:0 0 7px 20px;float:right;background:#fff;z-index:1} +@media only screen and (min-width:1760px){.markdown:not(code) .auto-toc-wrapper{right:-31%;width:26%;padding:0;margin-top:-3em!important;margin-left:-26%} +} +@media only screen and (max-width:479px){.markdown:not(code) .auto-toc-wrapper{float:none;width:100%;padding:0;margin-bottom:1em} +} +.markdown:not(code) .auto-toc-container{padding:7px;border:1px solid #d4d4d5;border-radius:5px} +.markdown:not(code) .auto-toc-container h2{padding:.3em;font-size:1.65em} +.markdown:not(code) .auto-toc-clear{clear:both;margin-bottom:-20px!important} +.markdown:not(code) .page-toc-wrapper{z-index:1;background:#fff;position:relative;display:none} +@media only screen and (min-width:1760px){.markdown:not(code) .page-toc-wrapper{display:block;float:left;left:-31%;width:26%;padding:0;margin-top:-3em!important;margin-right:-26%} +} +.markdown:not(code) .page-toc-wrapper a.selected,.markdown:not(code) .page-toc-wrapper span.selected{font-weight:700;font-style:italic} +.markdown:not(code) .page-toc-wrapper a.error,.markdown:not(code) .page-toc-wrapper span.error{color:#f22} +.markdown:not(code) .page-toc-wrapper .page-toc-close{display:none} +.markdown:not(code) #page-toc-wiki-cb{display:none} +@media only screen and (max-width:1759px){.markdown:not(code) #page-toc-wiki-cb:checked+.page-toc-wrapper{display:block;position:absolute;width:100%;max-width:420px;border:1px solid #404552;border-radius:5px;padding:20px;background:#fff;margin:-3em -3em 3em!important;z-index:5} +.markdown:not(code) .page-toc-wrapper .page-toc-close{display:inline-block;float:right;margin-top:-.6em!important;margin-right:-.2em!important} +} +label.ui.basic.button.page-toc-wiki-label{display:none;float:left;padding:11px!important;margin-right:10px!important} +@media only screen and (max-width:1759px){label.ui.basic.button.page-toc-wiki-label{display:block} +} +label.ui.basic.button.page-toc-wiki-label i{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .home .logo{max-width:220px} @media only screen and (max-width:767px){.home .hero h1{font-size:3.5em} .home .hero h2{font-size:2em} diff --git a/public/css/theme-arc-green.css b/public/css/theme-arc-green.css index b48e22b41e1c5..05dbc8d62b1e9 100644 --- a/public/css/theme-arc-green.css +++ b/public/css/theme-arc-green.css @@ -98,6 +98,9 @@ footer{background:#2e323e;border-top:1px solid #313131} .repository.file.editor.edit,.repository.wiki.new .CodeMirror{border-right:1px solid rgba(187,187,187,.6);border-left:1px solid rgba(187,187,187,.6);border-bottom:1px solid rgba(187,187,187,.6)} .repository.file.editor.edit .editor-preview,.repository.file.editor.edit .editor-preview-side,.repository.wiki.new .CodeMirror .editor-preview,.repository.wiki.new .CodeMirror .editor-preview-side{background:#353945} .repository.file.editor.edit .editor-preview .markdown:not(code).ui.segment,.repository.file.editor.edit .editor-preview-side .markdown:not(code).ui.segment,.repository.wiki.new .CodeMirror .editor-preview .markdown:not(code).ui.segment,.repository.wiki.new .CodeMirror .editor-preview-side .markdown:not(code).ui.segment{border-width:0} +.markdown:not(code) .auto-toc-wrapper,.markdown:not(code) .page-toc-wrapper{background:#353945} +.markdown:not(code) .auto-toc-container{background:#2a2e3a;border-color:#404552} +.markdown:not(code) #page-toc-wiki-cb:checked+.page-toc-wrapper{border-color:#d4d4d5;background:#383c4a} .ui.dropdown .menu{background:#2c303a} .ui.dropdown .menu>.message:not(.ui){color:#636363} .ui.input{color:#dbdbdb} diff --git a/public/js/index.js b/public/js/index.js index 22e4f9d419b2c..05b9ebd44f4f7 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1134,18 +1134,21 @@ function initWikiForm() { forceSync: true, previewRender: function (plainText, preview) { // Async method setTimeout(function () { - // FIXME: still send render request when return back to edit mode - $.post($editArea.data('url'), { - "_csrf": csrf, - "mode": "gfm", - "context": $editArea.data('context'), - "text": plainText - }, - function (data) { - preview.innerHTML = '
' + data + '
'; - emojify.run($('.editor-preview')[0]); - } - ); + let $toolbar = $(preview).closest('.CodeMirror-wrap').prev(); + if ($toolbar.hasClass('disabled-for-preview')) { + $.post($editArea.data('url'), { + "_csrf": csrf, + "mode": "gfm", + "wiki": true, + "context": decodeURIComponent($editArea.data('context')), + "text": plainText + }, + function (data) { + preview.innerHTML = '
' + data + '
'; + emojify.run($('.editor-preview')[0]); + } + ); + } }, 0); return "Loading..."; @@ -3065,3 +3068,295 @@ function onOAuthLoginClick() { oauthNav.show(); },5000); } +(function() { + // html listings ---------------------------------------------------- + let openedLists, listEopen; + // close list + const _closeList = function (count) { + let out = ''; + if(count == false || count == 0 || count == 'undefined' || typeof(count) == 'undefined' ) { + count = openedLists.length; + } else { + count = Math.min(count, openedLists.length); + } + while (count > 0){ + out += '' + openedLists.pop(); + listEopen = true; + count--; + } + return out; + }; + // open list + const _openList = function () { + let out = ''); + listEopen = false; + return out; + }; + // handle list element + // create valid html list + const __list = function (line, level, id) { + let out = ''; + let diff = level - openedLists.length; + if(diff > 0) { //open new level + out += _openList(); + out += __list(line,level, id); + } else if(diff < 0 ) { + out += _closeList(-diff); + out += __list(line, level, id); + } else { // only add list element + out += ((listEopen)?'':'') + '
  • ' + line + ''; + listEopen = true; + } + return out; + }; + // handle wiki page list element + // create valid html list + const __page_list = function (line, level, link, selected, error) { + let out = ''; + let diff = level - openedLists.length; + if(diff > 0) { //open new level + out += _openList(); + out += __page_list(line,level, link, selected, error); + } else if(diff < 0 ) { + out += _closeList(-diff); + out += __page_list(line, level, link, selected, error); + } else { // only add list element + let t = (link === null)? 'span' : 'a'; + let ref = (link === null)? '':' href="' + link + '"'; + let cl = '' + (error === true? ' error': '') + (selected?' selected':''); + let l = line; + try{ + l = decodeURIComponent(l); + try { + l = decodeURIComponent(l); + } catch (e) { + l = decodeURIComponent(line); + } + } catch (e) { + l = line; + } + out += ((listEopen)?'
  • ':'') + '
  • <'+t+' class="'+cl+'"'+ref+'>' + escapeHtml(l) + ''; + listEopen = true; + } + return out; + }; + /** + * find headlines and create list ---------------------------------- + * @param target Element target container where toc should be created + */ + const create_toc_inside = function(target) { + let rm; + if(target != null) { + if( (rm = target.querySelector('.auto-toc-wrapper')) != null ) { + rm.parentNode.removeChild(rm); + } + //remove optional toc flag + let ps = target.querySelectorAll('p'); + //look for toc keywoard + for (let i = 0; i < ps.length; i++) { + if (ps[i].textContent.trim() == '%%TOC%%') { + ps[i].parentNode.removeChild(ps[i]); + } + } + openedLists = []; listEopen = false; + // get content and create html + const elms = target.querySelectorAll('h1,h2,h3,h4,h5'); + if (elms.length > 0) { + let html = ''; + for(let i = 0; i < elms.length; i++){ + let l = elms[i].tagName.substr(1); //level + let t = elms[i].innerText.trim().trim(''); //text + let id = elms[i].id; + // create html + if(t.length > 0 && l >= 1) { + html += __list(t, l, id); + } else { + html += _closeList(0); + } + } + html += _closeList(0); + //create elements + let d = document.createElement('div'); + d.id = 'auto-toc'; + d.className = 'anchor-wrap'; + d.innerHTML = '

    '+((typeof(target.dataset.toc) == 'string' && target.dataset.toc != '')?target.dataset.toc:'Table of Contents')+'

    '; + let d2 = document.createElement('div'); + d2.className = 'auto-toc-container'; + d2.innerHTML = html; + d2.insertBefore(d, d2.firstChild); + let c = document.createElement('div'); + c.className = 'auto-toc-wrapper'; + c.appendChild(d2); + //inject toc + target.insertBefore(c, target.firstChild); + if( (rm = document.querySelector('.auto-toc-clear')) != null ) { + rm.parentNode.removeChild(rm); + } + let a = document.createElement('div'); a.className = 'auto-toc-clear'; + target.appendChild(a); + } + } + }; + /** + * search for %%TOC%% inside document if found create toc -------------------------- + * @param target Element target container where toc should be created + */ + const detect_toc_flag = function(target) { + if(target != null) { + let ps = target.querySelectorAll('p'); + let found = false; + //look for toc keywoard + for (let i = 0; i < ps.length; i++) { + if (ps[i].textContent.trim() == '%%TOC%%') { + found = ps[i]; + break; + } + } + if(found !== false) { + //remove toc keywoard + found.parentNode.removeChild(found); + //create toc + create_toc_inside(target); + } + } + }; + const escapeHtml = function (text) { + var map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); + } + // wiki page toc + // wiki page toc + const wiki_page_toc = function(target, data) { + let rm; + if(target != null && data != null) { + //remove page toc + if( (rm = document.querySelector('.page-toc-wrapper')) != null ) { + rm.parentNode.removeChild(rm); + } + if (data.length > 0) { + //get base path + let dl = [], tl = []; + let l = ''; + let ll = 0; + for(let i = 0; i < data.length; i++){ + if (i == 0) { + ll = data[i].dataset.url.indexOf('/wiki/', 0) + 6; + l = data[i].dataset.url.substr(0, ll); + } + let o = {li: data[i].dataset.url.substr(ll), t: '', l: 0, s: false, p: '', al:true}; + o.l = (o.li.match(/(\/|%2F|%252F)/g) || []).length + 1; + let lisplit = o.li.split(/%252F|%2F|\//g); + o.t = lisplit.pop(); + o.p = lisplit.join('%2F'); + tl.push(o.li); + if (data[i].className.indexOf('selected') != -1) o.s = true; + //add missing objects without link + dl.push(o); + } + // compare paths + const sortArr = function(a, b){ + let _a = decodeURIComponent(a.li).toLowerCase().split('/'); + let _b = decodeURIComponent(b.li).toLowerCase().split('/'); + // split and compare path components + for (let i = 0; i < Math.max(_a.length, _b.length); i++ ) { + // end if one path is shorter + if (i >= _a.length) return -1; + if (i >= _b.length) return 1; + // compare string components + if(_a[i] < _b[i]) return -1; + if(_a[i] > _b[i]) return 1; + } + // equals + return 0; + }; + dl.sort(sortArr); + // fill missing elements + for(let i = 0; i < dl.length; i++){ + if (dl[i].p != ''){ + let pathsplit = dl[i].p.split('%2F'); + for(let j = 0; j < pathsplit.length; j++){ + let p = pathsplit.slice(0, j + 1).join('%2F'); + if (p != '' && tl.indexOf(p) == -1){ + let o = {li: p, t: '', l: (p.match(/(\/|%2F|%252F)/g) || []).length + 1, s: false, p: '', al:false}; + let lisplit = o.li.split(/%252F|%2F|\//g); + o.t = lisplit.pop(); + tl.push(p); + //add missing objects without link + dl.push(o); + } + } + } + } + dl.sort(sortArr); + //create list + openedLists = []; listEopen = false; + // get content and create html + let html = ''; + let last_link = ''; + for(let i = 0; i < dl.length; i++){ + // create html + if(dl[i].t.length > 0 && dl[i].l >= 1) { + html += __page_list( dl[i].t, dl[i].l, (dl[i].al? l+dl[i].li : null), dl[i].s, (last_link !== l+dl[i].li && decodeURIComponent(last_link) !== l+dl[i].li && last_link !== decodeURIComponent(l+dl[i].li))? false : true ); + } else { + html += _closeList(0); + } + last_link = l+dl[i].li; + } + html += _closeList(0); + //create elements + let d = document.createElement('div'); + d.id = 'auto-page-toc'; + d.className = 'anchor-wrap'; + d.innerHTML = '

    '+((typeof(target.dataset.pagetoc) == 'string' && target.dataset.pagetoc != '')?target.dataset.pagetoc:'Table of Pages')+'

    '; + let d2 = document.createElement('div'); + d2.className = 'auto-toc-container'; + d2.innerHTML = html; + d2.insertBefore(d, d2.firstChild); + let c = document.createElement('div'); + c.className = 'page-toc-wrapper'; + c.appendChild(d2); + //inject page toc + target.insertBefore(c, target.firstChild); + if( (rm = document.querySelector('.auto-toc-clear')) == null ) { + let a = document.createElement('div'); a.className = 'auto-toc-clear'; + target.appendChild(a); + } + // small screen toggle + if( (rm = document.querySelector('.page-toc-label')) == null ) { + let a = document.createElement('label'); + a.className = 'page-toc-wiki-label ui basic button'; a.htmlFor = 'page-toc-wiki-cb'; + a.title = ((typeof(target.dataset.pagetoc) == 'string' && target.dataset.pagetoc != '')?target.dataset.pagetoc:'Table of Pages'); + a.innerHTML = ''; + let ltarget = document.querySelector('.repository.wiki .ui.container .ui.dividing.header .ui.stackable.grid').firstElementChild; + ltarget.insertBefore(a, ltarget.firstChild); + let cb = document.createElement('input'); + cb.type = 'checkbox'; cb.id = 'page-toc-wiki-cb'; cb.hidden = true; + target.insertBefore(cb, target.firstChild); + // may update height if toc size > container + cb.addEventListener("click", function(){ + if (cb.checked && target.offsetHeight < c.offsetHeight) { + target.style.minHeight = c.offsetHeight + 'px'; + } else { + target.style.removeProperty('min-height'); + } + return false; + }); + } + } + } + }; + // create toc ---------------------------------- + addEventListener("load", function(){ + create_toc_inside(document.querySelector('.file-view.markdown.auto-toc')); // md + detect_toc_flag( document.querySelector('.file-view.markdown.auto-toc-by-flag')); // md by %%TOC%% flag + create_toc_inside(document.querySelector('.segment.markdown.auto-toc')); // wiki pages + wiki_page_toc(document.querySelector('.segment.markdown.page-toc'), document.querySelectorAll('.wiki .choose.page .menu .item')); // wiki pages toc + }); +})(); diff --git a/public/less/_markdown.less b/public/less/_markdown.less index e971248f6ffca..ad280d5cc5750 100644 --- a/public/less/_markdown.less +++ b/public/less/_markdown.less @@ -1,5 +1,4 @@ .markdown:not(code) { - overflow: hidden; font-size: 16px; line-height: 1.6 !important; word-wrap: break-word; @@ -493,4 +492,122 @@ ul.ui.list ul { padding-left: 2em; } + + .auto-toc-wrapper { + position: relative; + padding: 0 0 7px 20px; + float: right; + background: #ffffff; + z-index: 1; + + @media only screen and (min-width: 1760px) { + right: -31%; + width: 26%; + padding: 0; + margin-top: -3em !important; + margin-left: -26%; + } + + @media only screen and (max-width: 479px) { + float: none; + width: 100%; + padding: 0; + margin-bottom: 1em; + } + + } + + .auto-toc-container { + padding: 7px; + border: 1px solid #d4d4d5; + border-radius: 5px; + + h2 { + padding: 0.3em; + font-size: 1.65em; + } + } + + .auto-toc-clear { + clear: both; + margin-bottom: -20px !important; + } + + .page-toc-wrapper { + z-index: 1; + background: #ffffff; + position: relative; + display: none; + + @media only screen and (min-width: 1760px) { + display: block; + float: left; + left: -31%; + width: 26%; + padding: 0; + margin-top: -3em !important; + margin-right: -26%; + } + + a.selected, + span.selected { + font-weight: bold; + font-style: italic; + } + + a.error, + span.error { + color: #ff2222; + } + + .page-toc-close { + display: none; + } + } + + #page-toc-wiki-cb { + display: none; + } + + @media only screen and (max-width: 1759px) { + #page-toc-wiki-cb:checked + .page-toc-wrapper { + display: block; + position: absolute; + width: 100%; + max-width: 420px; + border: 1px solid #404552; + border-radius: 5px; + padding: 20px; + background: #ffffff; + margin: -3em -3em 3em !important; + z-index: 5; + } + + .page-toc-wrapper .page-toc-close { + display: inline-block; + float: right; + margin-top: -0.6em !important; + margin-right: -0.2em !important; + } + } +} + +label.ui.basic.button.page-toc-wiki-label { + display: none; + float: left; + padding: 11px !important; + margin-right: 10px !important; + + @media only screen and (max-width: 1759px) { + display: block; + } + + i { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } } diff --git a/public/less/themes/arc-green.less b/public/less/themes/arc-green.less index cde5b5e7f50bb..5f83039548652 100644 --- a/public/less/themes/arc-green.less +++ b/public/less/themes/arc-green.less @@ -530,6 +530,21 @@ a.ui.basic.green.label:hover { } } +.markdown:not(code) .page-toc-wrapper, +.markdown:not(code) .auto-toc-wrapper { + background: #353945; +} + +.markdown:not(code) .auto-toc-container { + background: #2a2e3a; + border-color: #404552; +} + +.markdown:not(code) #page-toc-wiki-cb:checked + .page-toc-wrapper { + border-color: #d4d4d5; + background: #383c4a; +} + .ui.dropdown .menu { background: #2c303a; } diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 757295069e90c..a48b676a03dec 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -294,6 +294,29 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { ctx.ServerError("UpdateRepositoryUnits", err) return } + + // update toc settings + if repo.TocWikiTree != form.TocWikiTree { + repo.TocWikiTree = form.TocWikiTree + } + + if repo.TocWikiFile != form.TocWikiFile { + repo.TocWikiFile = form.TocWikiFile + } + + if repo.TocMarkupAlways != form.TocMarkupAlways { + repo.TocMarkupAlways = form.TocMarkupAlways + } + + if repo.TocMarkupByFlag != form.TocMarkupByFlag { + repo.TocMarkupByFlag = form.TocMarkupByFlag + } + + if err := models.UpdateRepository(repo, false); err != nil { + ctx.ServerError("UpdateRepository", err) + return + } + log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) diff --git a/routers/repo/view.go b/routers/repo/view.go index edaf24017ce6d..b75ce5d3dec1e 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -12,6 +12,7 @@ import ( gotemplate "html/template" "io/ioutil" "path" + "path/filepath" "strings" "code.gitea.io/gitea/models" @@ -163,6 +164,23 @@ func renderDirectory(ctx *context.Context, treeLink string) { buf = templates.ToUTF8WithFallback(append(buf, d...)) if markup.Type(readmeFile.Name()) != "" { + // Check if extension matches TOC file list + tocExts := setting.Markdown.TocMarkupFileExtensions + isTocMarkup := false + if len(tocExts) == 0 || len(tocExts) == 1 && tocExts[0] == "" || len(tocExts) == 2 && tocExts[0] == "" && tocExts[1] == "" { + isTocMarkup = true + } else { + fileExt := strings.ToLower(filepath.Ext(readmeFile.Name())) + if fileExt != "" { + for _, tExt := range tocExts { + if tExt == fileExt { + isTocMarkup = true + break + } + } + } + } + ctx.Data["IsTocMarkup"] = isTocMarkup ctx.Data["IsMarkup"] = true ctx.Data["FileContent"] = string(markup.Render(readmeFile.Name(), buf, treeLink, ctx.Repo.Repository.ComposeMetas())) } else { @@ -283,6 +301,23 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st readmeExist := markup.IsReadmeFile(blob.Name()) ctx.Data["ReadmeExist"] = readmeExist if markup.Type(blob.Name()) != "" { + // Check if extension matches TOC file list + tocExts := setting.Markdown.TocMarkupFileExtensions + isTocMarkup := false + if len(tocExts) == 0 || len(tocExts) == 1 && tocExts[0] == "" || len(tocExts) == 2 && tocExts[0] == "" && tocExts[1] == "" { + isTocMarkup = true + } else { + fileExt := strings.ToLower(filepath.Ext(blob.Name())) + if fileExt != "" { + for _, tExt := range tocExts { + if tExt == fileExt { + isTocMarkup = true + break + } + } + } + } + ctx.Data["IsTocMarkup"] = isTocMarkup ctx.Data["IsMarkup"] = true ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeMetas())) } else if readmeExist { diff --git a/routers/repo/wiki.go b/routers/repo/wiki.go index 43149c034061b..026dce17ef004 100644 --- a/routers/repo/wiki.go +++ b/routers/repo/wiki.go @@ -8,6 +8,7 @@ package repo import ( "fmt" "io/ioutil" + "net/url" "path/filepath" "strings" @@ -62,7 +63,7 @@ type PageMeta struct { // findEntryForFile finds the tree entry for a target filepath. func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) { - entries, err := commit.ListEntries() + entries, err := commit.ListEntriesRecursive() if err != nil { return nil, err } @@ -126,12 +127,11 @@ func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *gi } return nil, nil } - // Get page list. if isViewPage { - entries, err := commit.ListEntries() + entries, err := commit.Tree.ListEntriesRecursive() if err != nil { - ctx.ServerError("ListEntries", err) + ctx.ServerError("ListEntriesRecursive", err) return nil, nil } pages := make([]PageMeta, 0, len(entries)) @@ -139,25 +139,27 @@ func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *gi if !entry.IsRegular() { continue } - wikiName, err := models.WikiFilenameToName(entry.Name()) + wikiUnescapedName, wikiName, err := models.WikiFilenameToName(entry.Name()) if err != nil { if models.IsErrWikiInvalidFileName(err) { continue } - ctx.ServerError("WikiFilenameToName", err) - return nil, nil - } else if wikiName == "_Sidebar" || wikiName == "_Footer" { + if _, ok := err.(url.EscapeError); !ok { + ctx.ServerError("WikiFilenameToName", err) + return nil, nil + } + } else if wikiUnescapedName == "_Sidebar" || wikiUnescapedName == "_Footer" { continue } pages = append(pages, PageMeta{ - Name: wikiName, + Name: wikiUnescapedName, SubURL: models.WikiNameToSubURL(wikiName), }) } ctx.Data["Pages"] = pages } - pageName := models.NormalizeWikiName(ctx.Params(":page")) + pageName := ctx.Params(":page") if len(pageName) == 0 { pageName = "Home" } @@ -168,14 +170,23 @@ func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *gi ctx.Data["title"] = pageName ctx.Data["RequireHighlightJS"] = true - pageFilename := models.WikiNameToFilename(pageName) + // check new file structure with directories + pageFilename := models.WikiNameToPathFilename(pageName) var entry *git.TreeEntry + // old wiki pages are all inside git base directory if entry, err = findEntryForFile(commit, pageFilename); err != nil { ctx.ServerError("findEntryForFile", err) return nil, nil } else if entry == nil { - ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages") - return nil, nil + // check old file structure without directories + pageFilename = models.WikiNameToFilename(pageName) + if entry, err = findEntryForFile(commit, pageFilename); err != nil { + ctx.ServerError("findEntryForFile", err) + return nil, nil + } else if entry == nil { + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages") + return nil, nil + } } data := wikiContentsByEntry(ctx, entry) if ctx.Written() { @@ -194,7 +205,7 @@ func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *gi } metas := ctx.Repo.Repository.ComposeMetas() - ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas) + ctx.Data["content"] = markdown.RenderWiki(data, models.WikiNameToRawPrefix(ctx.Repo.RepoLink, pageFilename), metas) ctx.Data["sidebarPresent"] = sidebarPresent ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas) ctx.Data["footerPresent"] = footerPresent @@ -263,31 +274,43 @@ func WikiPages(ctx *context.Context) { return } - entries, err := commit.ListEntries() + entries, err := commit.Tree.ListEntriesRecursive() + fmt.Printf("%v", entries) if err != nil { - ctx.ServerError("ListEntries", err) + ctx.ServerError("ListEntriesRecursive", err) return } pages := make([]PageMeta, 0, len(entries)) +LoopPages: for _, entry := range entries { + blacklistWikifiles := strings.Split(".gitignore,.git", ",") + for _, b := range blacklistWikifiles { + if entry.Name() == b || strings.HasSuffix(entry.Name(), b) { + continue LoopPages + } + } + if !entry.IsRegular() { continue } + c, err := wikiRepo.GetCommitByPath(entry.Name()) if err != nil { ctx.ServerError("GetCommit", err) return } - wikiName, err := models.WikiFilenameToName(entry.Name()) + wikiUnescapedName, wikiName, err := models.WikiFilenameToName(entry.Name()) if err != nil { if models.IsErrWikiInvalidFileName(err) { continue } - ctx.ServerError("WikiFilenameToName", err) - return + if _, ok := err.(url.EscapeError); !ok { + ctx.ServerError("WikiFilenameToName", err) + return + } } pages = append(pages, PageMeta{ - Name: wikiName, + Name: wikiUnescapedName, SubURL: models.WikiNameToSubURL(wikiName), UpdatedUnix: util.TimeStamp(c.Author.When.Unix()), }) @@ -350,6 +373,13 @@ func NewWiki(ctx *context.Context) { if !ctx.Repo.Repository.HasWiki() { ctx.Data["title"] = "Home" + } else if len(ctx.Params(":page")) != 0 { + // create files on same subdiretory like current file + wikiName := ctx.Params(":page") + // remove current filename + a := strings.Split(wikiName, "/") + a[len(a)-1] = "New Page" + ctx.Data["title"] = strings.Join(a, "/") } ctx.HTML(200, tplWikiNew) @@ -371,7 +401,7 @@ func NewWikiPost(ctx *context.Context, form auth.NewWikiForm) { return } - wikiName := models.NormalizeWikiName(form.Title) + wikiName := form.Title if err := ctx.Repo.Repository.AddWikiPage(ctx.User, wikiName, form.Content, form.Message); err != nil { if models.IsErrWikiReservedName(err) { ctx.Data["Err_Title"] = true @@ -418,20 +448,26 @@ func EditWikiPost(ctx *context.Context, form auth.NewWikiForm) { return } - oldWikiName := models.NormalizeWikiName(ctx.Params(":page")) - newWikiName := models.NormalizeWikiName(form.Title) + oldWikiName := ctx.Params(":page") + newWikiName := form.Title if err := ctx.Repo.Repository.EditWikiPage(ctx.User, oldWikiName, newWikiName, form.Content, form.Message); err != nil { ctx.ServerError("EditWikiPage", err) return } + // redirect to may changed filename + newWikiName = models.FilenameToPathFilename(models.WikiNameToPathFilename(newWikiName)) + if strings.HasSuffix(newWikiName, ".md") { + newWikiName = newWikiName[:len(newWikiName)-3] + } + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + models.WikiNameToSubURL(newWikiName)) } // DeleteWikiPagePost delete wiki page func DeleteWikiPagePost(ctx *context.Context) { - wikiName := models.NormalizeWikiName(ctx.Params(":page")) + wikiName := ctx.Params(":page") if len(wikiName) == 0 { wikiName = "Home" } diff --git a/routers/repo/wiki_test.go b/routers/repo/wiki_test.go index 4687d24f6b28d..ecda2c9fc8b5d 100644 --- a/routers/repo/wiki_test.go +++ b/routers/repo/wiki_test.go @@ -25,12 +25,15 @@ func wikiEntry(t *testing.T, repo *models.Repository, wikiName string) *git.Tree assert.NoError(t, err) commit, err := wikiRepo.GetBranchCommit("master") assert.NoError(t, err) - entries, err := commit.ListEntries() + entries, err := commit.ListEntriesRecursive() assert.NoError(t, err) for _, entry := range entries { if entry.Name() == models.WikiNameToFilename(wikiName) { return entry } + if entry.Name() == models.WikiNameToPathFilename(wikiName) { + return entry + } } return nil } @@ -78,7 +81,7 @@ func TestWiki(t *testing.T) { Wiki(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) assert.EqualValues(t, "Home", ctx.Data["Title"]) - assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name"}, ctx.Data["Pages"]) + assertPagesMetas(t, []string{"Home", "Page-With-Image", "Page-With-Spaced-Name"}, ctx.Data["Pages"]) } func TestWikiPages(t *testing.T) { @@ -88,7 +91,7 @@ func TestWikiPages(t *testing.T) { test.LoadRepo(t, ctx, 1) WikiPages(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) - assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name"}, ctx.Data["Pages"]) + assertPagesMetas(t, []string{"Home", "Page-With-Image", "Page-With-Spaced-Name"}, ctx.Data["Pages"]) } func TestNewWiki(t *testing.T) { @@ -179,7 +182,7 @@ func TestEditWikiPost(t *testing.T) { func TestDeleteWikiPagePost(t *testing.T) { models.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/Home/delete") + ctx := test.MockContext(t, "user2/repo1/wiki/Home/_delete") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) DeleteWikiPagePost(ctx) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 744088a9d7b1e..afd53a361836d 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -808,9 +808,10 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("", func() { m.Combo("/_new").Get(repo.NewWiki). Post(bindIgnErr(auth.NewWikiForm{}), repo.NewWikiPost) + m.Get("/:page/_new/", repo.NewWiki) m.Combo("/:page/_edit").Get(repo.EditWiki). Post(bindIgnErr(auth.NewWikiForm{}), repo.EditWikiPost) - m.Post("/:page/delete", repo.DeleteWikiPagePost) + m.Post("/:page/_delete", repo.DeleteWikiPagePost) }, context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter) }, repo.MustEnableWiki, context.RepoRef()) diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index c6d715acbee88..4cb6d9f545fec 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -262,6 +262,37 @@ {{end}} +
    + +
    +
    + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 6445ee7b9e58d..db4e8824e125c 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -45,7 +45,7 @@
    -
    +
    {{if .IsMarkup}} {{if .FileContent}}{{.FileContent | Safe}}{{end}} {{else if .IsRenderedHTML}} diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl index bf6c24220bdde..2b8aa477208e7 100644 --- a/templates/repo/wiki/new.tmpl +++ b/templates/repo/wiki/new.tmpl @@ -7,22 +7,27 @@ {{.i18n.Tr "repo.wiki.new_page"}} {{if .PageIsWikiEdit}} {{end}}
    + {{if .PageIsWikiEdit}}
    + {{else}} + + {{end}} {{.CsrfTokenHtml}}
    - +
    + {{.i18n.Tr "repo.wiki.abort_edit_page_button"}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index dd2de2a041380..5d3de135639c0 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -66,8 +66,8 @@ {{if and .CanWriteWiki (not .Repository.IsMirror)}} {{end}}
    @@ -79,7 +79,7 @@
    {{end}}
    -
    +
    {{.content | Str2html}}
    {{if .sidebarPresent}}