Skip to content

Commit

Permalink
[Feature] Virtual Pages via JSON stream
Browse files Browse the repository at this point in the history
  • Loading branch information
SchumacherFM committed Mar 2, 2015
1 parent ab5862c commit 0f32b37
Show file tree
Hide file tree
Showing 10 changed files with 614 additions and 4 deletions.
14 changes: 13 additions & 1 deletion commands/hugo.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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 Source, SourceUrl, MenuUrl, CacheDir, Destination, Theme, BaseUrl, CfgFile, LogFile, Editor string

//Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
func Execute() {
Expand All @@ -83,6 +83,8 @@ func init() {
HugoCmd.PersistentFlags().BoolVar(&DisableRSS, "disableRSS", false, "Do not build RSS files")
HugoCmd.PersistentFlags().BoolVar(&DisableSitemap, "disableSitemap", false, "Do not build Sitemap file")
HugoCmd.PersistentFlags().StringVarP(&Source, "source", "s", "", "filesystem path to read files relative from")
HugoCmd.PersistentFlags().StringVar(&SourceUrl, "sourceUrl", "", "URL to streamed JSON source")
HugoCmd.PersistentFlags().StringVar(&MenuUrl, "menuUrl", "", "URL to streamed JSON menu")
HugoCmd.PersistentFlags().StringVarP(&CacheDir, "cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")
HugoCmd.PersistentFlags().BoolVarP(&IgnoreCache, "ignoreCache", "", false, "Ignores the cache directory for reading but still writes to it")
HugoCmd.PersistentFlags().StringVarP(&Destination, "destination", "d", "", "filesystem path to write files to")
Expand Down Expand Up @@ -118,6 +120,8 @@ func InitializeConfig() {
viper.SetDefault("DisableRSS", false)
viper.SetDefault("DisableSitemap", false)
viper.SetDefault("ContentDir", "content")
viper.SetDefault("SourceUrl", "")
viper.SetDefault("MenuUrl", "")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("StaticDir", "static")
viper.SetDefault("ArchetypeDir", "archetypes")
Expand Down Expand Up @@ -206,6 +210,14 @@ func InitializeConfig() {
viper.Set("WorkingDir", dir)
}

if SourceUrl != "" {
viper.Set("SourceUrl", SourceUrl)
}

if MenuUrl != "" {
viper.Set("MenuUrl", MenuUrl)
}

if hugoCmdV.PersistentFlags().Lookup("ignoreCache").Changed {
viper.Set("IgnoreCache", IgnoreCache)
}
Expand Down
47 changes: 47 additions & 0 deletions helpers/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright © 2014 Steve Francia <[email protected]>.
//
// 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 helpers

import (
"encoding/json"
"net/http"
"strings"

"github.com/spf13/afero"
jww "github.com/spf13/jwalterweatherman"
)

func JsonDecoder(url string, hc *http.Client, fs afero.Fs) (*json.Decoder, error) {
if url == "" {
return nil, nil
}
if strings.Contains(url, "://") {
jww.INFO.Printf("Downloading content JSON: %s ...", url)
res, err := hc.Get(url)
if err != nil {
return nil, err
}
return json.NewDecoder(res.Body), nil
}

if e, err := Exists(url, fs); !e {
return nil, err
}

f, err := fs.Open(url)
if err != nil {
return nil, err
}
return json.NewDecoder(f), nil
}
44 changes: 44 additions & 0 deletions hugolib/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ package hugolib

import (
"html/template"
"io"
"net/http"
"sort"
"strings"

"github.com/spf13/afero"
"github.com/spf13/cast"
"github.com/spf13/hugo/helpers"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
)

type MenuEntry struct {
Expand Down Expand Up @@ -183,3 +189,41 @@ func (m Menu) Reverse() Menu {

return m
}

// MenuLoadFromJson loads a JSON stream from a remote or local source
func MenuLoadFromJson(hc *http.Client, fs afero.Fs) map[string]interface{} {
url := viper.GetString("MenuUrl")
if url == "" {
return nil
}

if hc == nil {
hc = http.DefaultClient
}

dec, err := helpers.JsonDecoder(url, hc, fs)
if err != nil || dec == nil {
jww.ERROR.Printf("Failed to get menu json resource \"%s\" with error message: %s", url, err)
return nil
}

c := 0
ret := make(map[string]interface{})
jww.INFO.Printf("Generating menu from JSON %s", url)
for {
var si map[string]interface{}
if err := dec.Decode(&si); err == io.EOF {
jww.INFO.Printf("Generated %d menu entries from JSON stream", c)
break
} else if err != nil {
jww.WARN.Printf("Parser Error in JSON stream: %s", err.Error())
} else {
for k, v := range si {
ret[k] = v
}
c++
}
}

return ret
}
38 changes: 38 additions & 0 deletions hugolib/menu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
"strings"
"testing"

"bytes"

"github.com/BurntSushi/toml"
"github.com/spf13/afero"
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/hugofs"
"github.com/spf13/hugo/source"
"github.com/spf13/viper"
Expand Down Expand Up @@ -58,6 +61,14 @@ const (
name = "Unicode Russian"
identifier = "unicode-russian"
url = "/новости-проекта"` // Russian => "news-project"

MENU_JSON_STREAM = `{"main":[{"name":"Download Hugo","pre":"\u003ci class='fa fa-download'\u003e\u003c/i\u003e","url":"https://github.com/spf13/hugo/releases","weight":-200},{"name":"Showcase","pre":"\u003ci class='fa fa-cubes'\u003e\u003c/i\u003e","url":"/showcase/","weight":-180},{"name":"Discuss Hugo","pre":"\u003ci class='fa fa-comments'\u003e\u003c/i\u003e","url":"http://discuss.gohugo.io/","weight":-150},{"identifier":"about","name":"About Hugo","pre":"\u003ci class='fa fa-heart'\u003e\u003c/i\u003e","weight":-110},{"identifier":"getting started","name":"Getting Started","pre":"\u003ci class='fa fa-road'\u003e\u003c/i\u003e","weight":-100},{"identifier":"content","name":"Content","pre":"\u003ci class='fa fa-file-text'\u003e\u003c/i\u003e","weight":-90},{"identifier":"themes","name":"Themes","pre":"\u003ci class='fa fa-desktop'\u003e\u003c/i\u003e","weight":-85},{"identifier":"layout","name":"Templates","pre":"\u003ci class='fa fa-columns'\u003e\u003c/i\u003e","weight":-80},{"identifier":"taxonomy","name":"Taxonomies","pre":"\u003ci class='fa fa-tags'\u003e\u003c/i\u003e","weight":-70},{"identifier":"extras","name":"Extras","pre":"\u003ci class='fa fa-gift'\u003e\u003c/i\u003e","weight":-60},{"identifier":"community","name":"Community","pre":"\u003ci class='fa fa-group'\u003e\u003c/i\u003e","weight":-50},{"name":"Discussion Forum","parent":"community","url":"http://discuss.gohugo.io","weight":150},{"identifier":"tutorials","name":"Tutorials","pre":"\u003ci class='fa fa-book'\u003e\u003c/i\u003e","weight":-40},{"identifier":"troubleshooting","name":"Troubleshooting","pre":"\u003ci class='fa fa-wrench'\u003e\u003c/i\u003e","weight":-30}]}
{"footer":[{"name":"Download Hugo","pre":"\u003ci class='fa fa-download'\u003e\u003c/i\u003e","url":"https://github.com/spf13/hugo/releases","weight":-200},{"name":"Showcase","pre":"\u003ci class='fa fa-cubes'\u003e\u003c/i\u003e","url":"/showcase/","weight":-180},{"name":"Discuss Hugo","pre":"\u003ci class='fa fa-comments'\u003e\u003c/i\u003e","url":"http://discuss.gohugo.io/","weight":-150},{"identifier":"about","name":"About Hugo","pre":"\u003ci class='fa fa-heart'\u003e\u003c/i\u003e","weight":-110},{"identifier":"getting started","name":"Getting Started","pre":"\u003ci class='fa fa-road'\u003e\u003c/i\u003e","weight":-100},{"identifier":"content","name":"Content","pre":"\u003ci class='fa fa-file-text'\u003e\u003c/i\u003e","weight":-90},{"identifier":"themes","name":"Themes","pre":"\u003ci class='fa fa-desktop'\u003e\u003c/i\u003e","weight":-85},{"identifier":"layout","name":"Templates","pre":"\u003ci class='fa fa-columns'\u003e\u003c/i\u003e","weight":-80},{"identifier":"taxonomy","name":"Taxonomies","pre":"\u003ci class='fa fa-tags'\u003e\u003c/i\u003e","weight":-70},{"identifier":"extras","name":"Extras","pre":"\u003ci class='fa fa-gift'\u003e\u003c/i\u003e","weight":-60},{"identifier":"community","name":"Community","pre":"\u003ci class='fa fa-group'\u003e\u003c/i\u003e","weight":-50},{"name":"Discussion Forum","parent":"community","url":"http://discuss.gohugo.io","weight":150},{"identifier":"tutorials","name":"Tutorials","pre":"\u003ci class='fa fa-book'\u003e\u003c/i\u003e","weight":-40},{"identifier":"troubleshooting","name":"Troubleshooting","pre":"\u003ci class='fa fa-wrench'\u003e\u003c/i\u003e","weight":-30}]}
{"grandparent":[{"identifier":"grandparentId","name":"grandparent","url":"/grandparent"},{"identifier":"parentId","name":"parent","parent":"grandparentId","url":"/parent"},{"identifier":"grandchildId","name":"Go Home3","parent":"parentId","url":"/"}]}
{"main2":[{"name":"Go Home","post":"\u003c/div\u003e","pre":"\u003cdiv\u003e","url":"/","weight":1},{"name":"Blog","url":"/posts"}]}
{"tax":[{"identifier":"1","name":"Tax1","url":"/two/key/"},{"identifier":"2","name":"Tax2","url":"/two/key"},{"identifier":"xml","name":"Tax RSS","url":"/two/key.xml"}]}
{"unicode":[{"identifier":"unicode-russian","name":"Unicode Russian","url":"/новости-проекта"}]}
`
)

var MENU_PAGE_1 = []byte(`+++
Expand Down Expand Up @@ -506,5 +517,32 @@ func tomlToMap(s string) (map[string]interface{}, error) {
}

return data, nil
}

func TestMenuLoadFromJson(t *testing.T) {

fs := new(afero.MemMapFs)

r := bytes.NewReader([]byte(MENU_JSON_STREAM))
err := helpers.WriteToDisk("menu.json", r, fs)
if err != nil {
t.Error(err)
}

viper.Set("MenuUrl", "menu.json")
m := MenuLoadFromJson(nil, fs)
viper.Set("MenuUrl", "")

if len(m) != 6 {
t.Errorf("Expected 6 menu items but got %d", len(m))
}

if _, ok := m["grandparent"]; !ok {
t.Errorf("Missing menu item grandparent!")
}

mNil := MenuLoadFromJson(nil, fs)
if mNil != nil {
t.Error("MenuUrl is empty but the map is not!")
}
}
28 changes: 25 additions & 3 deletions hugolib/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,12 +421,13 @@ func (s *Site) initialize() (err error) {
}

staticDir := helpers.AbsPathify(viper.GetString("StaticDir") + "/")

s.Source = &source.Filesystem{
files := &source.Filesystem{
AvoidPaths: []string{staticDir},
Base: s.absContentDir(),
}

s.Source = source.MergeUrl(files, hugofs.SourceFs)

s.Menus = Menus{}

s.initializeSiteInfo()
Expand Down Expand Up @@ -681,11 +682,32 @@ func (s *Site) BuildSiteMeta() (err error) {
return
}

// getMenuRaw loads first a JSON based menu from a local or remote source
// and merges then the menu from the config file into the existing menu and overrides
// existing values.
func (s *Site) getMenuRaw() map[string]interface{} {
m := MenuLoadFromJson(nil, hugofs.SourceFs)
if m == nil {
m = make(map[string]interface{})
}

if vm := viper.GetStringMap("menu"); vm != nil {
for k, v := range vm {
if _, ok := m[k]; ok {
jww.INFO.Printf("Menu: overriding key: %s\n", k)
}
m[k] = v
}
}

return m
}

func (s *Site) getMenusFromConfig() Menus {

ret := Menus{}

if menus := viper.GetStringMap("menu"); menus != nil {
if menus := s.getMenuRaw(); menus != nil {
for name, menu := range menus {
m, err := cast.ToSliceE(menu)
if err != nil {
Expand Down
74 changes: 74 additions & 0 deletions source/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright © 2014 Steve Francia <[email protected]>.
//
// 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 source

import (
"io"
"net/http"

"bytes"
"path/filepath"

"github.com/spf13/afero"
"github.com/spf13/hugo/helpers"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
)

type (
jsonPage struct {
FilePath string `json:"Path"`
Content string `json:"Content"`
}
)

func (p *jsonPage) Reader() io.Reader {
return bytes.NewReader([]byte(p.Content))
}

func (p *jsonPage) Path() string {
return filepath.Clean(p.FilePath)
}

// jsonStreamToFiles acts as the main function to be called in url.go
func loadJson(hc *http.Client, fs afero.Fs) []Pager {
url := viper.GetString("SourceUrl")
if url == "" {
return nil
}

dec, err := helpers.JsonDecoder(url, hc, fs)
if err != nil || dec == nil {
jww.ERROR.Printf("Failed to get json resource \"%s\" with error message: %s", url, err)
return nil
}

c := 0
sources := make([]Pager, 0, 1000)
jww.INFO.Printf("Generating files from JSON %s", url)
for {
var s jsonPage
if err := dec.Decode(&s); err == io.EOF {
jww.INFO.Printf("Generated %d file/s from JSON stream", c)
break
} else if err != nil {
jww.WARN.Printf("Parser Error in JSON stream: %s", err.Error())
} else {
sources = append(sources, &s)
c++
}
}

return sources
}
Loading

0 comments on commit 0f32b37

Please sign in to comment.