From 84b62ac053c7d6816412a2034402cf845fc12c90 Mon Sep 17 00:00:00 2001 From: bep Date: Mon, 23 Feb 2015 18:12:19 +0100 Subject: [PATCH] Use osext on GitHub Fixes #922 add finalize command and tests to change draft status of specified content to false and set date to Now() undraft command Allow hyphens in shortcode name Fixes #929 Keep trailing slash when baseUrl contains a sub path Before this commit, .Site.BaseUrl ended up as: http://mysite.com/ => http://mysite.com/ http://mysite.com/sub/ => http://mysite.com/sub Now it becomes: http://mysite.com/ => http://mysite.com/ http://mysite.com/sub/ => http://mysite.com/sub/ Fixed #931 Improve error message on missing shortcode inner content Fixes #933 Allow the same shortcode to be used with or without inline content Fixes #934 Update Readme.md with additional contribution guides Added Gitter badge Restructure top of Readme.md Fixing image in readme Using a smaller Header Image Tidying the Readme a bit more Update github-pages-blog.md change `hugo serve` to `hugo server` Add benchmark for the shortcode lexer Apply gofmt -s Avoid panic when pagination on 0 pages Fixes #948 Prevent 404.html from prettifying into 404/index.html Restore @realchaseadams's commit 348e123 "Force `UglyUrls` option to force `404.html` file name" which got lost after some refactoring (commit 8db3c0b). Remove the equivalent "force `UglyUrls`" code for `sitemap.xml` because the refactored code now calls `renderAndWriteXML()` which uses `WriteDestFile()` which does not prettify a filename. Fixes #939 Fix errors reported by Go Vet Fix some Go Lint errors Apply some more Golint suggestions helpers: apply some Golint rules author: fix doc hugolib: apply some Hugolint rules page: make receiver name on Page methods consistent Apply some Golint rules on Page, esp. making the method receiver names consistent: (page *Page) ==> (p *Page) menu: make receiver name on Page methods consistent Apply some Golint rules on Menu, esp. making the method receiver names consistent. hugolib: apply some more Golint rules source: apply some Golint rules livereload: apply some Golint rules parser: apply some Golint rules There is only one s.PageTarget() - so we cannot change it, even tempoararily. We have to find another solution to this. ... Prevent 404.html from prettifying into 404/index.html Restore @realchaseadams's commit 348e123 "Force `UglyUrls` option to force `404.html` file name" which got lost after some refactoring (commit 8db3c0b). Remove the equivalent "force `UglyUrls`" code for `sitemap.xml` because the refactored code now calls `renderAndWriteXML()` which uses `WriteDestFile()` which does not prettify a filename. Fixes #939 (reverted from commit c4c19ad303cb11616a7291bdbeec997e59b6d24e) Handle 404 thread safely Replaces hack that temporarily changes a global flag. Fixes #955 Fixes #939 Fix UglyUrls on Windows Fixes #958 Fix eq and ne tpl function issue `eq` and `ne` template functions don't work as expected when those are used with a raw number and a calculated value by add, sub etc. It's caused by both numbers type differences. For example, `eq 5 (add 2 3)` returns `false` because raw 5 is `int` while `add 2 3` returns 5 with `int64` This normalizes `int`, `uint` and `float` type values to `int64`, `uint64` and `float64` before comparing them. Other type of value is passed to comparing function without any changes. Fix #961 Add test cases for Ne and Eq type normalisation See #961 Add new min_version field to theme.toml template Switch from fsnotify.v0 to fsnotify.v1 API (watcher) Fixes #357 See also #761 Do not parse backup files with trailing '~' as templates Fixes #964 absurlreplacer: write replacements directly to the byte buffer The extra step isn't needed and this makes the code simpler. And slightly faster: benchmark old ns/op new ns/op delta BenchmarkAbsUrl 19987 17498 -12.45% BenchmarkXmlAbsUrl 10606 9503 -10.40% benchmark old allocs new allocs delta BenchmarkAbsUrl 28 24 -14.29% BenchmarkXmlAbsUrl 14 12 -14.29% benchmark old bytes new bytes delta BenchmarkAbsUrl 3512 3297 -6.12% BenchmarkXmlAbsUrl 2059 1963 -4.66% parser: add some frontmatter test cases Skip directories like node_modules from the watchlist A local `node_modules` directory can easily contain tens of thousands of files, easily exhausting the tiny default max open files limit especially on OS X Yosemite, in spite of the fact that Hugo already had code in place since February 2014 to try to raise the maxfiles ulimit. Also skip `.git` and `bower_components` directories. The file watching situation will improve when https://github.com/go-fsnotify/fsevents become ready, but until then, we will be thrifty. :-) Thanks to @chibicode for the suggestion. See #168 for continued discussions. Add some basic tests for doArithmetic We might have to take precision into account for floating point nubers ... at some point. doArithmetic: add test for division by zero Correct initialisms as suggested by golint First step to use initialisms that golint suggests, for example: Line 116: func GetHtmlRenderer should be GetHTMLRenderer as see on http://goreportcard.com/report/spf13/hugo Thanks to @bep for the idea! Note that command-line flags (cobra and pflag) as well as struct fields like .BaseUrl and .Url that are used in Go HTML templates need more work to maintain backward-compatibility, and thus are NOT yet dealt with in this commit. First step in fixing #959. Remove trailing space from site build statistics Update press coverage: Fix URL; new tutorial in Chinese [Docs] Update and expand http://gohugo.io/overview/usage/ The `hugo help` output as shown in http://gohugo.io/overview/usage/ was not yet updated for v0.13. Thanks to @alebaffa for the heads up! Also: - Clarify that, after using `hugo server`, the bare `hugo` command need to be run before deployment. - Add a section on running `hugo` as production web server, and add links to two blog posts of two Hugo users sharing their experience. Partially fixes: #852 and #937 Add deprecated logger Add double checking in Deprecated To prevent possible duplicate log statements. source: add some test cases for File Do not ERROR-log missing /data dir Fixes #930 Experimental AsciiDoc support with external helpers See #470 * Based on existing support for reStructuredText files * Handles content files with extensions `.asciidoc` and `.ad` * Pipes content through `asciidoctor --safe -`. If `asciidoctor` is not installed, then `asciidoc --safe -`. * To make sure `asciidoctor` or `asciidoc` is found, after adding a piece of AsciiDoc content, run `hugo` with the `-v` flag and look for this message: INFO: 2015/01/23 Rendering with /usr/bin/asciidoctor ... Caveats: * The final "Last updated" timestamp is currently not stripped. * When `hugo` is run with `-v`, you may see a lot of these messages INFO: 2015/01/23 Rendering with /usr/bin/asciidoctor ... if you have lots of `*.ad`, `*.adoc` or `*.asciidoc` files. * Some versions of `asciidoc` may have trouble with its safe mode. To test if you are affected, try this: $ echo "Hello" | asciidoc --safe - asciidoc: ERROR: unsafe: ifeval invalid asciidoc: FAILED: ifeval invalid safe document If so, I recommend that you install `asciidoctor` instead. Feedback and patches welcome! Ideally, we should be using https://github.com/VonC/asciidocgo, @VonC's wonderful Go implementation of Asciidoctor. However, there is still a bit of work needed for asciidocgo to expose its API so that Hugo can actually use it. Until then, hope this "experimental AsciiDoc support through external helpers" can serve as a stopgap solution for our community. :-) 2015-01-30: Updated for the replaceShortcodeTokens() syntax change 2015-02-21: Add `.adoc` extension as suggested by @Fale Conflicts: helpers/content.go Add Seq template func Very similar to GNU's seq. Fixes #552 Conflicts: tpl/template.go Added image support to the sitemap.xml template Conflicts: tpl/template_embedded.go Revert "Added image support to the sitemap.xml template" This reverts commit 3c147bd41922c4f0ac81bc1e1f2ffab8093d5d1c. Fixes #972 removed duplicate word in readme delete finalize files --- README.md | 23 +- bufferpool/bufpool.go | 3 + commands/convert.go | 14 +- commands/hugo.go | 77 ++++--- commands/new.go | 8 +- commands/server.go | 12 +- commands/server_test.go | 14 +- commands/undraft.go | 157 +++++++++++++ commands/undraft_test.go | 72 ++++++ commands/version.go | 2 +- create/content.go | 4 +- docs/content/community/press.md | 3 +- docs/content/extras/shortcodes.md | 4 + docs/content/overview/usage.md | 195 +++++++++++----- docs/content/tutorials/github-pages-blog.md | 2 +- helpers/content.go | 125 +++++++---- helpers/content_test.go | 4 +- helpers/general.go | 106 ++++++++- helpers/general_test.go | 54 ++++- helpers/path.go | 27 +-- helpers/path_test.go | 8 +- helpers/url.go | 81 ++++--- helpers/url_test.go | 72 ++++-- hugolib/author.go | 8 +- hugolib/handler_base.go | 4 +- hugolib/handler_page.go | 23 +- hugolib/media.go | 4 +- hugolib/menu.go | 30 ++- hugolib/menu_test.go | 94 ++++---- hugolib/node.go | 6 +- hugolib/page.go | 236 ++++++++++---------- hugolib/pageSort.go | 10 +- hugolib/page_permalink_test.go | 8 +- hugolib/pagesPrevNext.go | 4 +- hugolib/pagination.go | 41 ++-- hugolib/pagination_test.go | 21 +- hugolib/path_separators_windows_test.go | 2 +- hugolib/permalinks.go | 4 +- hugolib/shortcode.go | 21 +- hugolib/shortcode_test.go | 15 +- hugolib/shortcodeparser.go | 8 +- hugolib/shortcodeparser_test.go | 45 ++-- hugolib/site.go | 102 ++++----- hugolib/site_show_plan_test.go | 2 +- hugolib/site_test.go | 121 ++++++++-- hugolib/siteinfo_test.go | 2 +- hugolib/taxonomy.go | 24 +- livereload/livereload.go | 2 +- parser/frontmatter.go | 18 +- parser/frontmatter_test.go | 25 +++ parser/page.go | 10 +- source/content_directory_test.go | 4 +- source/file.go | 30 ++- source/file_test.go | 25 +++ source/filesystem.go | 18 +- target/page.go | 4 +- target/page_test.go | 2 +- tpl/template.go | 77 ++++--- tpl/template_embedded.go | 4 +- tpl/template_resources.go | 18 +- tpl/template_resources_test.go | 4 +- tpl/template_test.go | 94 +++++--- transform/absurl.go | 10 +- transform/absurlreplacer.go | 207 +++++++---------- transform/chain_test.go | 8 +- utils/utils.go | 6 +- utils/utils_test.go | 18 +- watcher/batcher.go | 27 ++- 68 files changed, 1644 insertions(+), 869 deletions(-) create mode 100644 commands/undraft.go create mode 100644 commands/undraft_test.go create mode 100644 parser/frontmatter_test.go create mode 100644 source/file_test.go diff --git a/README.md b/README.md index 88dfe7bb499..6d0d3636ca3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ -# Hugo +![Hugo](https://raw.githubusercontent.com/spf13/hugo/master/docs/static/img/hugo-logo.png) + A Fast and Flexible Static Site Generator built with love by [spf13](http://spf13.com/) and [friends](https://github.com/spf13/hugo/graphs/contributors) in [Go][]. -[![Build Status](https://travis-ci.org/spf13/hugo.png)](https://travis-ci.org/spf13/hugo) [![wercker status](https://app.wercker.com/status/1a0de7d703ce3b80527f00f675e1eb32 "wercker status")](https://app.wercker.com/project/bykey/1a0de7d703ce3b80527f00f675e1eb32) [![Build status](https://ci.appveyor.com/api/projects/status/n2mo912b8s2505e8/branch/master?svg=true)](https://ci.appveyor.com/project/spf13/hugo/branch/master) +[Website](http://gohugo.io) | +[Forum](https://discuss.gohugo.io) | +[Chat](https://gitter.im/spf13/hugo) | +[Documentation](http://gohugo.io/overview/introduction/) | +[Installation Guide](http://gohugo.io/overview/installing/) | +[Twitter](http://twitter.com/spf13) + +[![Build Status](https://travis-ci.org/spf13/hugo.png)](https://travis-ci.org/spf13/hugo) [![wercker status](https://app.wercker.com/status/1a0de7d703ce3b80527f00f675e1eb32 "wercker status")](https://app.wercker.com/project/bykey/1a0de7d703ce3b80527f00f675e1eb32) [![Build status](https://ci.appveyor.com/api/projects/status/n2mo912b8s2505e8/branch/master?svg=true)](https://ci.appveyor.com/project/spf13/hugo/branch/master) [![Join the chat at https://gitter.im/spf13/hugo](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/hugo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Overview @@ -74,7 +82,16 @@ To update Hugo’s dependencies, use `go get` with the `-u` option. go get -u -v github.com/spf13/hugo -## Contributing Code +## Contributing to Hugo + +We welcome contributions to Hugo of any kind including documentation, themes, organization, tutorials, blog posts, bug reports, issues, feature requests, feature implementation, pull requests, answering questions on the forum, helping to manage issues, etc. The Hugo community and maintainers are very active and helpful and the project benefits greatly from this activity. + +[![Throughput Graph](https://graphs.waffle.io/spf13/hugo/throughput.svg)](https://waffle.io/spf13/hugo/metrics) + +If you have any questions about how to contribute or what to contribute please ask on the [forum](http://discuss.gohugo.io) + + +## Code Contribution Guide Contributors should build Hugo and test their changes before submitting a code change. diff --git a/bufferpool/bufpool.go b/bufferpool/bufpool.go index 0b7829b2cf1..5a550e0e7cb 100644 --- a/bufferpool/bufpool.go +++ b/bufferpool/bufpool.go @@ -24,10 +24,13 @@ var bufferPool = &sync.Pool{ }, } +// GetBuffer returns a buffer from the pool. func GetBuffer() (buf *bytes.Buffer) { return bufferPool.Get().(*bytes.Buffer) } +// PutBuffer returns a buffer to the pool. +// The buffer is reset before it is put back into circulation. func PutBuffer(buf *bytes.Buffer) { buf.Reset() bufferPool.Put(buf) diff --git a/commands/convert.go b/commands/convert.go index ec6e2e5518d..6268fb2a236 100644 --- a/commands/convert.go +++ b/commands/convert.go @@ -27,8 +27,8 @@ import ( "github.com/spf13/viper" ) -var OutputDir string -var Unsafe bool +var outputDir string +var unsafe bool var convertCmd = &cobra.Command{ Use: "convert", @@ -80,8 +80,8 @@ func init() { convertCmd.AddCommand(toJSONCmd) convertCmd.AddCommand(toTOMLCmd) convertCmd.AddCommand(toYAMLCmd) - convertCmd.PersistentFlags().StringVarP(&OutputDir, "output", "o", "", "filesystem path to write files to") - convertCmd.PersistentFlags().BoolVar(&Unsafe, "unsafe", false, "enable less safe operations, please backup first") + convertCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "filesystem path to write files to") + convertCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "enable less safe operations, please backup first") } func convertContents(mark rune) (err error) { @@ -134,10 +134,10 @@ func convertContents(mark rune) (err error) { page.SetSourceContent(psr.Content()) page.SetSourceMetaData(metadata, mark) - if OutputDir != "" { - page.SaveSourceAs(filepath.Join(OutputDir, page.FullFilePath())) + if outputDir != "" { + page.SaveSourceAs(filepath.Join(outputDir, page.FullFilePath())) } else { - if Unsafe { + if unsafe { page.SaveSource() } else { jww.FEEDBACK.Println("Unsafe operation not allowed, use --unsafe or set a different output path") diff --git a/commands/hugo.go b/commands/hugo.go index 1b19c34a4b8..3a6bb5142c3 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -1,4 +1,4 @@ -// Copyright © 2013 Steve Francia . +// Copyright © 2013-2015 Steve Francia . // // Licensed under the Simple Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import ( jww "github.com/spf13/jwalterweatherman" "github.com/spf13/nitro" "github.com/spf13/viper" + "gopkg.in/fsnotify.v1" ) //HugoCmd is Hugo's root command. Every other command attached to HugoCmd is a child command to it. @@ -55,8 +56,8 @@ Complete documentation is available at http://gohugo.io`, var hugoCmdV *cobra.Command //Flags that are to be added to commands. -var BuildWatch, IgnoreCache, Draft, Future, UglyUrls, Verbose, Logging, VerboseLog, DisableRSS, DisableSitemap, PluralizeListTitles, NoTimes bool -var Source, CacheDir, Destination, Theme, BaseUrl, CfgFile, LogFile, Editor string +var BuildWatch, IgnoreCache, Draft, Future, UglyURLs, Verbose, Logging, VerboseLog, DisableRSS, DisableSitemap, PluralizeListTitles, NoTimes bool +var Source, CacheDir, Destination, Theme, BaseURL, CfgFile, LogFile, Editor string //Execute adds all child commands to the root command HugoCmd and sets flags appropriately. func Execute() { @@ -74,6 +75,7 @@ func AddCommands() { HugoCmd.AddCommand(convertCmd) HugoCmd.AddCommand(newCmd) HugoCmd.AddCommand(listCmd) + HugoCmd.AddCommand(undraftCmd) } //Initializes flags @@ -88,8 +90,8 @@ func init() { HugoCmd.PersistentFlags().StringVarP(&Destination, "destination", "d", "", "filesystem path to write files to") HugoCmd.PersistentFlags().StringVarP(&Theme, "theme", "t", "", "theme to use (located in /themes/THEMENAME/)") HugoCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") - HugoCmd.PersistentFlags().BoolVar(&UglyUrls, "uglyUrls", false, "if true, use /filename.html instead of /filename/") - HugoCmd.PersistentFlags().StringVarP(&BaseUrl, "baseUrl", "b", "", "hostname (and path) to the root eg. http://spf13.com/") + HugoCmd.PersistentFlags().BoolVar(&UglyURLs, "uglyUrls", false, "if true, use /filename.html instead of /filename/") + HugoCmd.PersistentFlags().StringVarP(&BaseURL, "baseUrl", "b", "", "hostname (and path) to the root eg. http://spf13.com/") HugoCmd.PersistentFlags().StringVar(&CfgFile, "config", "", "config file (default is path/config.yaml|json|toml)") HugoCmd.PersistentFlags().StringVar(&Editor, "editor", "", "edit new content with this editor, if provided") HugoCmd.PersistentFlags().BoolVar(&Logging, "log", false, "Enable Logging") @@ -126,10 +128,10 @@ func InitializeConfig() { viper.SetDefault("DefaultLayout", "post") viper.SetDefault("BuildDrafts", false) viper.SetDefault("BuildFuture", false) - viper.SetDefault("UglyUrls", false) + viper.SetDefault("UglyURLs", false) viper.SetDefault("Verbose", false) viper.SetDefault("IgnoreCache", false) - viper.SetDefault("CanonifyUrls", false) + viper.SetDefault("CanonifyURLs", false) viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"}) viper.SetDefault("Permalinks", make(hugolib.PermalinkOverrides, 0)) viper.SetDefault("Sitemap", hugolib.Sitemap{Priority: -1}) @@ -154,7 +156,7 @@ func InitializeConfig() { } if hugoCmdV.PersistentFlags().Lookup("uglyUrls").Changed { - viper.Set("UglyUrls", UglyUrls) + viper.Set("UglyURLs", UglyURLs) } if hugoCmdV.PersistentFlags().Lookup("disableRSS").Changed { @@ -180,14 +182,14 @@ func InitializeConfig() { if hugoCmdV.PersistentFlags().Lookup("logFile").Changed { viper.Set("LogFile", LogFile) } - if BaseUrl != "" { - if !strings.HasSuffix(BaseUrl, "/") { - BaseUrl = BaseUrl + "/" + if BaseURL != "" { + if !strings.HasSuffix(BaseURL, "/") { + BaseURL = BaseURL + "/" } - viper.Set("BaseUrl", BaseUrl) + viper.Set("BaseURL", BaseURL) } - if viper.GetString("BaseUrl") == "" { + if viper.GetString("BaseURL") == "" { jww.ERROR.Println("No 'baseurl' set in configuration or as a flag. Features like page menus will not work without one.") } @@ -291,10 +293,17 @@ func copyStatic() error { return syncer.Sync(publishDir, staticDir) } +// getDirList provides NewWatcher() with a list of directories to watch for changes. func getDirList() []string { var a []string + dataDir := helpers.AbsPathify(viper.GetString("DataDir")) walker := func(path string, fi os.FileInfo, err error) error { if err != nil { + if path == dataDir && os.IsNotExist(err) { + jww.WARN.Println("Skip DataDir:", err) + return nil + + } jww.ERROR.Println("Walker: ", err) return nil } @@ -317,12 +326,16 @@ func getDirList() []string { } if fi.IsDir() { + if fi.Name() == ".git" || + fi.Name() == "node_modules" || fi.Name() == "bower_components" { + return filepath.SkipDir + } a = append(a, path) } return nil } - filepath.Walk(helpers.AbsPathify(viper.GetString("DataDir")), walker) + filepath.Walk(dataDir, walker) filepath.Walk(helpers.AbsPathify(viper.GetString("ContentDir")), walker) filepath.Walk(helpers.AbsPathify(viper.GetString("LayoutDir")), walker) filepath.Walk(helpers.AbsPathify(viper.GetString("StaticDir")), walker) @@ -349,7 +362,7 @@ func buildSite(watching ...bool) (err error) { return nil } -//NewWatcher creates a new watcher to watch filesystem events. +// NewWatcher creates a new watcher to watch filesystem events. func NewWatcher(port int) error { if runtime.GOOS == "darwin" { tweakLimit() @@ -369,50 +382,50 @@ func NewWatcher(port int) error { for _, d := range getDirList() { if d != "" { - _ = watcher.Watch(d) + _ = watcher.Add(d) } } go func() { for { select { - case evs := <-watcher.Event: + case evs := <-watcher.Events: jww.INFO.Println("File System Event:", evs) - static_changed := false - dynamic_changed := false - static_files_changed := make(map[string]bool) + staticChanged := false + dynamicChanged := false + staticFilesChanged := make(map[string]bool) for _, ev := range evs { ext := filepath.Ext(ev.Name) - istemp := strings.HasSuffix(ext, "~") || (ext == ".swp") || (ext == ".swx") || (ext == ".tmp") || (strings.HasPrefix(ext, ".goutputstream")) + istemp := strings.HasSuffix(ext, "~") || (ext == ".swp") || (ext == ".swx") || (ext == ".tmp") || strings.HasPrefix(ext, ".goutputstream") if istemp { continue } // renames are always followed with Create/Modify - if ev.IsRename() { + if ev.Op&fsnotify.Rename == fsnotify.Rename { continue } isstatic := strings.HasPrefix(ev.Name, helpers.GetStaticDirPath()) || strings.HasPrefix(ev.Name, helpers.GetThemesDirPath()) - static_changed = static_changed || isstatic - dynamic_changed = dynamic_changed || !isstatic + staticChanged = staticChanged || isstatic + dynamicChanged = dynamicChanged || !isstatic if isstatic { if staticPath, err := helpers.MakeStaticPathRelative(ev.Name); err == nil { - static_files_changed[staticPath] = true + staticFilesChanged[staticPath] = true } } // add new directory to watch list if s, err := os.Stat(ev.Name); err == nil && s.Mode().IsDir() { - if ev.IsCreate() { - watcher.Watch(ev.Name) + if ev.Op&fsnotify.Create == fsnotify.Create { + watcher.Add(ev.Name) } } } - if static_changed { + if staticChanged { jww.FEEDBACK.Println("Static file changed, syncing\n") utils.StopOnErr(copyStatic(), fmt.Sprintf("Error copying static files to %s", helpers.AbsPathify(viper.GetString("PublishDir")))) @@ -420,8 +433,8 @@ func NewWatcher(port int) error { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initalized // force refresh when more than one file - if len(static_files_changed) == 1 { - for path := range static_files_changed { + if len(staticFilesChanged) == 1 { + for path := range staticFilesChanged { livereload.RefreshPath(path) } @@ -431,7 +444,7 @@ func NewWatcher(port int) error { } } - if dynamic_changed { + if dynamicChanged { fmt.Print("\nChange detected, rebuilding site\n") const layout = "2006-01-02 15:04 -0700" fmt.Println(time.Now().Format(layout)) @@ -442,7 +455,7 @@ func NewWatcher(port int) error { livereload.ForceRefresh() } } - case err := <-watcher.Error: + case err := <-watcher.Errors: if err != nil { fmt.Println("error:", err) } diff --git a/commands/new.go b/commands/new.go index 781351e4638..5721b7f8892 100644 --- a/commands/new.go +++ b/commands/new.go @@ -220,13 +220,17 @@ func touchFile(x ...string) { func createThemeMD(inpath string) (err error) { - by := []byte(`name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `" + by := []byte(`# theme.toml template for a Hugo theme +# See https://github.com/spf13/hugoThemes#themetoml for an example + +name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `" license = "MIT" -licenselink = "https://github.com/.../.../LICENSE.md" +licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE.md" description = "" homepage = "http://siteforthistheme.com/" tags = ["", ""] features = ["", ""] +min_version = 0.13 [author] name = "" diff --git a/commands/server.go b/commands/server.go index 6b145449d97..29900f9ce54 100644 --- a/commands/server.go +++ b/commands/server.go @@ -84,11 +84,11 @@ func server(cmd *cobra.Command, args []string) { viper.Set("port", serverPort) - BaseUrl, err := fixUrl(BaseUrl) + BaseURL, err := fixURL(BaseURL) if err != nil { jww.ERROR.Fatal(err) } - viper.Set("BaseUrl", BaseUrl) + viper.Set("BaseURL", BaseURL) if err := memStats(); err != nil { jww.ERROR.Println("memstats error:", err) @@ -114,9 +114,9 @@ func serve(port int) { httpFs := &afero.HttpFs{SourceFs: hugofs.DestinationFS} fileserver := http.FileServer(httpFs.Dir(helpers.AbsPathify(viper.GetString("PublishDir")))) - u, err := url.Parse(viper.GetString("BaseUrl")) + u, err := url.Parse(viper.GetString("BaseURL")) if err != nil { - jww.ERROR.Fatalf("Invalid BaseUrl: %s", err) + jww.ERROR.Fatalf("Invalid BaseURL: %s", err) } if u.Path == "" || u.Path == "/" { http.Handle("/", fileserver) @@ -137,10 +137,10 @@ func serve(port int) { // fixUrl massages the BaseUrl into a form needed for serving // all pages correctly. -func fixUrl(s string) (string, error) { +func fixURL(s string) (string, error) { useLocalhost := false if s == "" { - s = viper.GetString("BaseUrl") + s = viper.GetString("BaseURL") useLocalhost = true } if !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") { diff --git a/commands/server_test.go b/commands/server_test.go index 1e1343d44c6..ea853e8016f 100644 --- a/commands/server_test.go +++ b/commands/server_test.go @@ -6,11 +6,11 @@ import ( "github.com/spf13/viper" ) -func TestFixUrl(t *testing.T) { +func TestFixURL(t *testing.T) { type data struct { TestName string - CliBaseUrl string - CfgBaseUrl string + CLIBaseURL string + CfgBaseURL string AppendPort bool Port int Result string @@ -28,13 +28,13 @@ func TestFixUrl(t *testing.T) { } for i, test := range tests { - BaseUrl = test.CliBaseUrl - viper.Set("BaseUrl", test.CfgBaseUrl) + BaseURL = test.CLIBaseURL + viper.Set("BaseURL", test.CfgBaseURL) serverAppend = test.AppendPort serverPort = test.Port - result, err := fixUrl(BaseUrl) + result, err := fixURL(BaseURL) if err != nil { - t.Errorf("Test #%d %s: unexpected error %s", err) + t.Errorf("Test #%d %s: unexpected error %s", i, test.TestName, err) } if result != test.Result { t.Errorf("Test #%d %s: expected %q, got %q", i, test.TestName, test.Result, result) diff --git a/commands/undraft.go b/commands/undraft.go new file mode 100644 index 00000000000..4dbdf45c927 --- /dev/null +++ b/commands/undraft.go @@ -0,0 +1,157 @@ +// Copyright © 2013 Steve Francia . +// +// Licensed under the Simple Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://opensource.org/licenses/Simple-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package commands + +import ( + "bytes" + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/hugo/parser" + jww "github.com/spf13/jwalterweatherman" +) + +var undraftCmd = &cobra.Command{ + Use: "undraft path/to/content", + Short: "Undraft changes the content's draft status from 'True' to 'False'", + Long: `Undraft changes the content's draft status from 'True' to 'False' and updates the date to the current date and time. If the content's draft status is 'False', nothing is done`, + Run: Undraft, +} + +// Publish publishes the specified content by setting its draft status +// to false and setting its publish date to now. If the specified content is +// not a draft, it will log an error. +func Undraft(cmd *cobra.Command, args []string) { + InitializeConfig() + + if len(args) < 1 { + cmd.Usage() + jww.FATAL.Fatalln("a piece of content needs to be specified") + } + + location := args[0] + // open the file + f, err := os.Open(location) + if err != nil { + jww.ERROR.Print(err) + return + } + + // get the page from file + p, err := parser.ReadFrom(f) + f.Close() + if err != nil { + jww.ERROR.Print(err) + return + } + + w, err := undraftContent(p) + if err != nil { + jww.ERROR.Printf("an error occurred while undrafting %q: %s", location, err) + return + } + + f, err = os.OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + jww.ERROR.Printf("%q not be undrafted due to error opening file to save changes: %q\n", location, err) + return + } + defer f.Close() + _, err = w.WriteTo(f) + if err != nil { + jww.ERROR.Printf("%q not be undrafted due to save error: %q\n", location, err) + } + return +} + +// undraftContent: if the content is a draft, change it's draft status to +// 'false' and set the date to time.Now(). If the draft status is already +// 'false', don't do anything. +func undraftContent(p parser.Page) (bytes.Buffer, error) { + var buff bytes.Buffer + // get the metadata; easiest way to see if it's a draft + meta, err := p.Metadata() + if err != nil { + return buff, err + } + // since the metadata was obtainable, we can also get the key/value separator for + // Front Matter + fm := p.FrontMatter() + if fm == nil { + err := fmt.Errorf("Front Matter was found, nothing was finalized") + return buff, err + } + + var isDraft, gotDate bool + var date string +L: + for k, v := range meta.(map[string]interface{}) { + switch k { + case "draft": + if !v.(bool) { + return buff, fmt.Errorf("not a Draft: nothing was done") + } + isDraft = true + if gotDate { + break L + } + case "date": + date = v.(string) // capture the value to make replacement easier + gotDate = true + if isDraft { + break L + } + } + } + + // if draft wasn't found in FrontMatter, it isn't a draft. + if !isDraft { + return buff, fmt.Errorf("not a Draft: nothing was done") + } + + // get the front matter as bytes and split it into lines + var lineEnding []byte + fmLines := bytes.Split(fm, parser.UnixEnding) + if len(fmLines) == 1 { // if the result is only 1 element, try to split on dos line endings + fmLines = bytes.Split(fm, parser.DosEnding) + if len(fmLines) == 1 { + return buff, fmt.Errorf("unable to split FrontMatter into lines") + } + lineEnding = append(lineEnding, parser.DosEnding...) + } else { + lineEnding = append(lineEnding, parser.UnixEnding...) + } + + // Write the front matter lines to the buffer, replacing as necessary + for _, v := range fmLines { + pos := bytes.Index(v, []byte("draft")) + if pos != -1 { + v = bytes.Replace(v, []byte("true"), []byte("false"), 1) + goto write + } + pos = bytes.Index(v, []byte("date")) + if pos != -1 { // if date field wasn't found, add it + v = bytes.Replace(v, []byte(date), []byte(time.Now().Format(time.RFC3339)), 1) + } + write: + buff.Write(v) + buff.Write(lineEnding) + } + + // append the actual content + buff.Write([]byte(p.Content())) + + return buff, nil +} diff --git a/commands/undraft_test.go b/commands/undraft_test.go new file mode 100644 index 00000000000..e0a654f3a71 --- /dev/null +++ b/commands/undraft_test.go @@ -0,0 +1,72 @@ +package commands + +// TODO Support Mac Encoding (\r) + +import ( + "bytes" + "strings" + "testing" + "time" + + "github.com/spf13/hugo/parser" +) + +var ( + jsonFM = "{\n \"date\": \"12-04-06\",\n \"title\": \"test json\"\n}" + jsonDraftFM = "{\n \"draft\": true,\n \"date\": \"12-04-06\",\n \"title\":\"test json\"\n}" + tomlFM = "+++\n date= \"12-04-06\"\n title= \"test toml\"\n+++" + tomlDraftFM = "+++\n draft= true\n date= \"12-04-06\"\n title=\"test toml\"\n+++" + yamlFM = "---\n date: \"12-04-06\"\n title: \"test yaml\"\n---" + yamlDraftFM = "---\n draft: true\n date: \"12-04-06\"\n title: \"test yaml\"\n---" +) + +func TestUndraftContent(t *testing.T) { + tests := []struct { + fm string + expectedErr string + }{ + {jsonFM, "not a Draft: nothing was done"}, + {jsonDraftFM, ""}, + {tomlFM, "not a Draft: nothing was done"}, + {tomlDraftFM, ""}, + {yamlFM, "not a Draft: nothing was done"}, + {yamlDraftFM, ""}, + } + + for _, test := range tests { + r := bytes.NewReader([]byte(test.fm)) + p, _ := parser.ReadFrom(r) + res, err := undraftContent(p) + if test.expectedErr != "" { + if err == nil { + t.Error("Expected error, got none") + continue + } + if err.Error() != test.expectedErr { + t.Errorf("Expected %q, got %q", test.expectedErr, err) + continue + } + } else { + r = bytes.NewReader(res.Bytes()) + p, _ = parser.ReadFrom(r) + meta, err := p.Metadata() + if err != nil { + t.Errorf("unexpected error %q", err) + continue + } + for k, v := range meta.(map[string]interface{}) { + if k == "draft" { + if v.(bool) { + t.Errorf("Expected %q to be \"false\", got \"true\"", k) + continue + } + } + if k == "date" { + if !strings.HasPrefix(v.(string), time.Now().Format("2006-01-02")) { + t.Errorf("Expected %v to start with %v", v.(string), time.Now().Format("2006-01-02")) + } + } + } + } + } +} diff --git a/commands/version.go b/commands/version.go index 799ff70df74..88ea941f7b1 100644 --- a/commands/version.go +++ b/commands/version.go @@ -20,7 +20,7 @@ import ( "strings" "time" - "bitbucket.org/kardianos/osext" + "github.com/kardianos/osext" "github.com/spf13/cobra" "github.com/spf13/hugo/hugolib" ) diff --git a/create/content.go b/create/content.go index fa37f266649..acb788c631d 100644 --- a/create/content.go +++ b/create/content.go @@ -63,7 +63,7 @@ func NewContent(kind, name string) (err error) { return err } - for k, _ := range newmetadata { + for k := range newmetadata { switch strings.ToLower(k) { case "date": newmetadata[k] = time.Now() @@ -73,7 +73,7 @@ func NewContent(kind, name string) (err error) { } caseimatch := func(m map[string]interface{}, key string) bool { - for k, _ := range m { + for k := range m { if strings.ToLower(k) == strings.ToLower(key) { return true } diff --git a/docs/content/community/press.md b/docs/content/community/press.md index eb6d3e604e8..eb2845e26f0 100644 --- a/docs/content/community/press.md +++ b/docs/content/community/press.md @@ -16,8 +16,9 @@ Hugo has been featured in the following Blog Posts, Press and Media. | Title | Author | Date | | ------ | ------ | -----: | +| [服务器上 hugo 的安装和配置 (Installing and configuring Hugo on the server)](http://hucsmn.com/post/hugo-tutorial-make-it-work/) | hucsmn | 11 Feb 2015 | | [把这个博客静态化了 (Migrate to Hugo)](http://lich-eng.com/2015/01/03/migrate-to-hugo/) | Li Cheng | 3 Jan 2015 | -| [My Hugo Experiment](http://baty.net/2014/12/2014-12-31-my-hugo-experiment/) | Jack Baty | 31 Dec 2014 | +| [My Hugo Experiment](http://tilde.club/~jbaty/2014/12/2014-12-31-my-hugo-experiment/) | Jack Baty | 31 Dec 2014 | | [6 Static Blog Generators That Aren’t Jekyll](http://www.sitepoint.com/6-static-blog-generators-arent-jekyll/) | David Turnbull | 8 Dec 2014 | | [Travel Blogging Setup](http://www.stou.dk/2014/11/travel-blogging-setup/) | Rasmus Stougaard | 23 Nov 2014 | | [使用Hugo搭建免费个人Blog (How to use Hugo)](http://ulricqin.com/post/how-to-use-hugo/) | Ulric Qin 秦晓辉 | 11 Nov 2014 | diff --git a/docs/content/extras/shortcodes.md b/docs/content/extras/shortcodes.md index ecb087082e9..79c6871cd0a 100644 --- a/docs/content/extras/shortcodes.md +++ b/docs/content/extras/shortcodes.md @@ -193,6 +193,10 @@ of the content between the opening and closing shortcodes. If a closing shortcode is required, you can check the length of `.Inner` and provide a warning to the user. +A shortcode with `.Inner` content can be used wihout the inline content, and without the closing shortcode, by using the self-closing syntax: + + {{}} + The variable `.Params` contains the list of parameters in case you need to do more complicated things than `.Get`. You can also use the variable `.Page` to access all the normal [Page Variables](/templates/variables/). diff --git a/docs/content/overview/usage.md b/docs/content/overview/usage.md index c5ab765d6f9..d7a3c4944a8 100644 --- a/docs/content/overview/usage.md +++ b/docs/content/overview/usage.md @@ -14,69 +14,154 @@ weight: 30 Make sure either `hugo` is in your `PATH` or provide a path to it. - $ hugo help - A Fast and Flexible Static Site Generator - built with love by spf13 and friends in Go. - - Complete documentation is available at http://gohugo.io - - Usage: - hugo [flags] - hugo [command] - - Available Commands: - server Hugo runs its own webserver to render the files - version Print the version number of Hugo - check Check content in the source directory - benchmark Benchmark hugo by building a site a number of times - new [path] Create new content for your site - help [command] Help about any command - - Available Flags: - -b, --baseUrl="": hostname (and path) to the root eg. http://spf13.com/ - -D, --buildDrafts=false: build content marked as draft - -F, --buildFuture=false: build content with PublishDate in the future - --config="": config file (default is path/config.yaml|json|toml) - -d, --destination="": filesystem path to write files to - --disableRSS=false: Do not build RSS files - --disableSitemap=false: Do not build Sitemap file - --log=false: Enable Logging - --logFile="": Log File path (if set, logging enabled automatically) - -s, --source="": filesystem path to read files relative from - --stepAnalysis=false: display memory and timing of different steps of the program - -t, --theme="": theme to use (located in /themes/THEMENAME/) - --uglyUrls=false: if true, use /filename.html instead of /filename/ - -v, --verbose=false: verbose output - --verboseLog=false: verbose logging - -w, --watch=false: watch filesystem for changes and recreate as needed - - Use "hugo help [command]" for more information about that command. +
$ hugo help
+A Fast and Flexible Static Site Generator built with
+love by spf13 and friends in Go.
+
+Complete documentation is available at http://gohugo.io
+
+Usage:
+  hugo [flags]
+  hugo [command]
+Available Commands:
+  server      Hugo runs its own webserver to render the files
+  version     Print the version number of Hugo
+  config      Print the site configuration
+  check       Check content in the source directory
+  benchmark   Benchmark hugo by building a site a number of times
+  new         Create new content for your site
+  help        Help about any command
+
+Flags:
+      --noTimes=false: Don't sync modification time of files
+  -w, --watch=false: watch filesystem for changes and recreate as needed
+
+Global Flags:
+  -b, --baseUrl="": hostname (and path) to the root eg. http://spf13.com/
+  -D, --buildDrafts=false: include content marked as draft
+  -F, --buildFuture=false: include content with datePublished in the future
+      --cacheDir="": filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/
+      --config="": config file (default is path/config.yaml|json|toml)
+  -d, --destination="": filesystem path to write files to
+      --disableRSS=false: Do not build RSS files
+      --disableSitemap=false: Do not build Sitemap file
+      --editor="": edit new content with this editor, if provided
+  -h, --help=false: help for hugo
+      --ignoreCache=false: Ignores the cache directory for reading but still writes to it
+      --log=false: Enable Logging
+      --logFile="": Log File path (if set, logging enabled automatically)
+      --pluralizeListTitles=true: Pluralize titles in lists using inflect
+  -s, --source="": filesystem path to read files relative from
+      --stepAnalysis=false: display memory and timing of different steps of the program
+  -t, --theme="": theme to use (located in /themes/THEMENAME/)
+      --uglyUrls=false: if true, use /filename.html instead of /filename/
+  -v, --verbose=false: verbose output
+      --verboseLog=false: verbose logging
+
+Use "hugo help [command]" for more information about a command.
+
## Common Usage Example -The most common use is probably to run `hugo` with your current directory being the input directory. +The most common use is probably to run `hugo` with your current directory being the input directory: $ hugo - > X pages created - in 8 ms + 0 draft content + 0 future content + 99 pages created + 0 paginator pages created + 16 tags created + 0 groups created + in 120 ms + +This generates your web site to the `public/` directory, +ready to be deployed to your web server. + + +## Instant feedback as you develop your web site If you are working on things and want to see the changes immediately, tell Hugo to watch for changes. +Hugo will watch the filesystem for changes, and rebuild your site as soon as a file is saved: + + $ hugo -s ~/Code/hugo/docs --watch + 0 draft content + 0 future content + 99 pages created + 0 paginator pages created + 16 tags created + 0 groups created + in 120 ms + Watching for changes in /Users/spf13/Code/hugo/docs/content + Press Ctrl+C to stop + +Hugo can even run a server and create a site preview at the same time! +Hugo implements [LiveReload](/extras/livereload/) technology to automatically +reload any open pages in all JavaScript-enabled browsers, including mobile. +This is the easiest and most common way to develop a Hugo web site: + + $ hugo server -ws ~/Code/hugo/docs + 0 draft content + 0 future content + 99 pages created + 0 paginator pages created + 16 tags created + 0 groups created + in 120 ms + Watching for changes in /Users/spf13/Code/hugo/docs/content + Serving pages from /Users/spf13/Code/hugo/docs/public + Web Server is available at http://localhost:1313/ + Press Ctrl+C to stop + + +## Deploying your web site + +After running `hugo server` for local web development, +you need to do a final `hugo` run **without the `server` command** +and **without `--watch` or `-w`** to rebuild your site. +You may then **deploy your site** by copying the `public/` directory +(by FTP, SFTP, WebDAV, Rsync, git push, etc.) to your production web server. + +Since Hugo generates a static website, your site can be hosted anywhere, +including [Heroku][], [GoDaddy][], [DreamHost][], [GitHub Pages][], +[Amazon S3][] and [CloudFront][], or any other cheap or even free +static web hosting services. + +[Apache][], [nginx][], [IIS][]... Any web server software would do! + +[Apache]: http://httpd.apache.org/ "Apache HTTP Server" +[nginx]: http://nginx.org/ +[IIS]: http://www.iis.net/ +[Heroku]: https://www.heroku.com/ +[GoDaddy]: https://www.godaddy.com/ +[DreamHost]: http://www.dreamhost.com/ +[GitHub Pages]: https://pages.github.com/ +[Amazon S3]: http://aws.amazon.com/s3/ +[CloudFront]: http://aws.amazon.com/cloudfront/ "Amazon CloudFront" + + +### Alternatively, serve your web site with Hugo! + +Yes, that's right! Because Hugo is so blazingly fast both in web site creation +*and* in web serving (thanks to its concurrent and multi-threaded design and +its Go heritage), some users actually prefer using Hugo itself to serve their +web site *on their production server*! + +No other web server software (Apache, nginx, IIS...) is necessary. + +Here is the command: + + hugo server --watch \ + --baseUrl=http://yoursite.org/ --port=80 \ + --appendPort=false -Hugo will watch the filesystem for changes, rebuild your site as soon as a file is saved. +This way, you may actually deploy just the source files, +and Hugo on your server will generate the resulting web site +on-the-fly and serve them at the same time. - $ hugo -s ~/mysite --watch - 28 pages created - in 18 ms - Watching for changes in /Users/spf13/Code/hugo/docs/content - Press Ctrl+C to stop +You may optionally add `--disableLiveReload=true` if you do not want +the JavaScript code for LiveReload to be added to your web pages. -Hugo can even run a server and create a site preview at the same time! Hugo -implements [LiveReload](/extras/livereload/) technology to automatically reload any open pages in all browsers (including mobile). (Note that you'll need to run without -w before you deploy your site.) +Interested? Here are some great tutorials contributed by Hugo users: - $ hugo server -ws ~/mysite - Watching for changes in /Users/spf13/Code/hugo/docs/content - Web Server is available at http://localhost:1313 - Press Ctrl+C to stop - 28 pages created - 0 tags created - in 18 ms \ No newline at end of file +* [hugo, syncthing](http://fredix.ovh/2014/10/hugo-syncthing/) (French) by Frédéric Logier (@fredix) +* [服务器上 hugo 的安装和配置 (Installing and configuring Hugo on the server)](http://hucsmn.com/post/hugo-tutorial-make-it-work/) (Chinese) by hucsmn diff --git a/docs/content/tutorials/github-pages-blog.md b/docs/content/tutorials/github-pages-blog.md index 69ae93ef77a..c32399226b1 100644 --- a/docs/content/tutorials/github-pages-blog.md +++ b/docs/content/tutorials/github-pages-blog.md @@ -251,7 +251,7 @@ Step by step: 1. Create on GitHub `-hugo` repository (it will host Hugo's content) 2. Create on GitHub `.github.io` repository (it will host the `public` folder: the static website) 2. `git clone <-hugo-url> && cd -hugo` -3. Make your website work locally (`hugo serve --watch -t `) +3. Make your website work locally (`hugo server --watch -t `) 4. Once you are happy with the results, Ctrl+C (kill server) and `rm -rf public` (don't worry, it can always be regenerated with `hugo -t `) 5. `git submodule add git@github.com:/.github.io.git public` 6. Almost done: add a `deploy.sh` script to help you (and make it executable: `chmod +x deploy.sh`): diff --git a/helpers/content.go b/helpers/content.go index 914fdfda51c..daf7e267da4 100644 --- a/helpers/content.go +++ b/helpers/content.go @@ -37,18 +37,20 @@ var SummaryLength = 70 // Custom divider let's user define where summarization ends. var SummaryDivider = []byte("") +// Blackfriday holds configuration values for Blackfriday rendering. type Blackfriday struct { AngledQuotes bool Fractions bool - PlainIdAnchors bool + PlainIDAnchors bool Extensions []string } +// NewBlackfriday creates a new Blackfriday with some sane defaults. func NewBlackfriday() *Blackfriday { return &Blackfriday{ AngledQuotes: false, Fractions: true, - PlainIdAnchors: false, + PlainIDAnchors: false, } } @@ -77,28 +79,27 @@ func StripHTML(s string) string { // Shortcut strings with no tags in them if !strings.ContainsAny(s, "<>") { return s - } else { - s = stripHTMLReplacer.Replace(s) - - // Walk through the string removing all tags - b := bp.GetBuffer() - defer bp.PutBuffer(b) - - inTag := false - for _, r := range s { - switch r { - case '<': - inTag = true - case '>': - inTag = false - default: - if !inTag { - b.WriteRune(r) - } + } + s = stripHTMLReplacer.Replace(s) + + // Walk through the string removing all tags + b := bp.GetBuffer() + defer bp.PutBuffer(b) + + inTag := false + for _, r := range s { + switch r { + case '<': + inTag = true + case '>': + inTag = false + default: + if !inTag { + b.WriteRune(r) } } - return b.String() } + return b.String() } // StripEmptyNav strips out empty