diff --git a/cmd/gojekyll/commands.go b/cmd/gojekyll/commands.go index 311ff63..a7af395 100644 --- a/cmd/gojekyll/commands.go +++ b/cmd/gojekyll/commands.go @@ -129,6 +129,10 @@ func renderCommand(site *sites.Site) error { printPathSetting("Render:", filepath.Join(site.Source, p.SiteRelPath())) printSetting("URL:", p.Permalink()) printSetting("Content:", "") + err = site.InitializeRenderingPipeline() + if err != nil { + return err + } return p.Write(site, os.Stdout) } diff --git a/config/config_test.go b/config/config_test.go index db8ad37..c84d06a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -19,3 +19,10 @@ func TestUnmarshal(t *testing.T) { require.Equal(t, "x", c.Source) require.Equal(t, "./_site", c.Destination) } + +func TestIsMarkdown(t *testing.T) { + c := Default() + require.True(t, c.IsMarkdown("name.md")) + require.True(t, c.IsMarkdown("name.markdown")) + require.False(t, c.IsMarkdown("name.html")) +} diff --git a/config/pathnames.go b/config/pathnames.go new file mode 100644 index 0000000..b7562b6 --- /dev/null +++ b/config/pathnames.go @@ -0,0 +1,36 @@ +package config + +import ( + "path/filepath" + "strings" + + "github.com/osteele/gojekyll/helpers" +) + +// IsMarkdown returns a boolean indicating whether the file is a Markdown file, according to the current project. +func (c *Config) IsMarkdown(name string) bool { + ext := filepath.Ext(name) + return c.markdownExtensions()[strings.TrimLeft(ext, ".")] +} + +// IsSassPath returns a boolean indicating whether the file is a Sass (".sass" or ".scss") file. +func (c *Config) IsSassPath(name string) bool { + return strings.HasSuffix(name, ".sass") || strings.HasSuffix(name, ".scss") +} + +// MarkdownExtensions returns a set of markdown extensions, without the initial dots. +func (c *Config) markdownExtensions() map[string]bool { + extns := strings.SplitN(c.MarkdownExt, `,`, -1) + return helpers.StringArrayToMap(extns) +} + +func (c *Config) OutputExt(pathname string) string { + switch { + case c.IsMarkdown(pathname): + return ".html" + case c.IsSassPath(pathname): + return ".css" + default: + return filepath.Ext(pathname) + } +} diff --git a/server/watcher.go b/server/watcher.go index c22c0bc..efa27bb 100644 --- a/server/watcher.go +++ b/server/watcher.go @@ -43,7 +43,7 @@ func (s *Server) watchFiles() error { log.Println("error:", err) continue } - url, found := site.RelPathURL(relpath) + url, found := site.RelativeFilenameToURL(relpath) if !found { // TODO don't warn re config and excluded files log.Println("error:", filename, "does not match a site URL") diff --git a/sites/build.go b/sites/build.go index 26d09d9..fe7d9d7 100644 --- a/sites/build.go +++ b/sites/build.go @@ -49,12 +49,10 @@ func (s *Site) Clean(options BuildOptions) error { // It attends to the global options.dry_run. func (s *Site) Build(options BuildOptions) (int, error) { count := 0 + s.InitializeRenderingPipeline() if err := s.Clean(options); err != nil { return count, err } - if err := s.CopySassFileIncludes(); err != nil { - return count, err - } if err := s.SetPageContentTemplateValues(); err != nil { return count, err } diff --git a/sites/layouts.go b/sites/layouts.go index 33fca2f..599a86a 100644 --- a/sites/layouts.go +++ b/sites/layouts.go @@ -13,9 +13,9 @@ import ( ) // FindLayout returns a template for the named layout. -func (s *Site) FindLayout(base string, fm *templates.VariableMap) (t liquid.Template, err error) { +func (p *Pipeline) FindLayout(base string, fm *templates.VariableMap) (t liquid.Template, err error) { exts := []string{"", ".html"} - for _, ext := range strings.SplitN(s.config.MarkdownExt, `,`, -1) { + for _, ext := range strings.SplitN(p.config.MarkdownExt, `,`, -1) { exts = append(exts, "."+ext) } var ( @@ -25,7 +25,7 @@ func (s *Site) FindLayout(base string, fm *templates.VariableMap) (t liquid.Temp ) for _, ext := range exts { // TODO respect layout config - name = filepath.Join(s.LayoutsDir(), base+ext) + name = filepath.Join(p.LayoutsDir(), base+ext) content, err = ioutil.ReadFile(name) if err == nil { found = true @@ -42,5 +42,10 @@ func (s *Site) FindLayout(base string, fm *templates.VariableMap) (t liquid.Temp if err != nil { return } - return s.TemplateEngine().Parse(content) + return p.liquidEngine.Parse(content) +} + +// LayoutsDir returns the path to the layouts directory. +func (p *Pipeline) LayoutsDir() string { + return filepath.Join(p.sourceDir, p.config.LayoutsDir) } diff --git a/sites/load.go b/sites/load.go index e5825a4..c36a15d 100644 --- a/sites/load.go +++ b/sites/load.go @@ -19,7 +19,6 @@ func (s *Site) Load() (err error) { if err != nil { return } - s.liquidEngine, err = s.makeLiquidEngine() return } @@ -32,7 +31,7 @@ func (s *Site) Reload() error { } copy.Destination = s.Destination *s = *copy - s.sassTempDir = "" + s.pipeline = nil return s.Load() } diff --git a/sites/markdown.go b/sites/markdown.go deleted file mode 100644 index 347d71d..0000000 --- a/sites/markdown.go +++ /dev/null @@ -1,20 +0,0 @@ -package sites - -import ( - "path/filepath" - "strings" - - "github.com/osteele/gojekyll/helpers" -) - -// IsMarkdown returns a boolean indicating whether the file is a Markdown file, according to the current project. -func (s *Site) IsMarkdown(name string) bool { - ext := filepath.Ext(name) - return s.MarkdownExtensions()[strings.TrimLeft(ext, ".")] -} - -// MarkdownExtensions returns a set of markdown extension, without the final dots. -func (s *Site) MarkdownExtensions() map[string]bool { - extns := strings.SplitN(s.config.MarkdownExt, `,`, -1) - return helpers.StringArrayToMap(extns) -} diff --git a/sites/pipeline.go b/sites/pipeline.go new file mode 100644 index 0000000..78b5ed2 --- /dev/null +++ b/sites/pipeline.go @@ -0,0 +1,154 @@ +package sites + +import ( + "io" + "io/ioutil" + "path/filepath" + + "github.com/osteele/gojekyll/config" + "github.com/osteele/gojekyll/helpers" + "github.com/osteele/gojekyll/liquid" + "github.com/osteele/gojekyll/pages" + "github.com/osteele/gojekyll/templates" + "github.com/russross/blackfriday" +) + +// Pipeline applies a rendering transformation to a file. +type Pipeline struct { + config config.Config + liquidEngine liquid.Engine + pageSupplier PageSupplier + sassTempDir string + sourceDir string +} + +// PipelineOptions configures a pipeline. +type PipelineOptions struct { + UseRemoteLiquidEngine bool +} + +// PageSupplier tells a pipeline how to resolve link tags. +type PageSupplier interface { + Pages() []pages.Page + RelativeFilenameToURL(string) (string, bool) +} + +// NewPipeline makes a rendering pipeline. +func NewPipeline(sourceDir string, c config.Config, pageSupplier PageSupplier, o PipelineOptions) (*Pipeline, error) { + p := Pipeline{config: c, pageSupplier: pageSupplier, sourceDir: sourceDir} + engine, err := p.makeLiquidEngine(o) + if err != nil { + return nil, err + } + p.liquidEngine = engine + if err := p.CopySassFileIncludes(); err != nil { + return nil, err + } + return &p, nil +} + +// OutputExt returns the output extension. +func (p *Pipeline) OutputExt(pathname string) string { + return p.config.OutputExt(pathname) +} + +// OutputExt returns the output extension. +func (s *Site) OutputExt(pathname string) string { + return s.config.OutputExt(pathname) +} + +// Render returns nil iff it wrote to the writer +func (p *Pipeline) Render(w io.Writer, b []byte, filename string, e templates.VariableMap) ([]byte, error) { + if p.config.IsSassPath(filename) { + return nil, p.WriteSass(w, b) + } + b, err := p.renderTemplate(b, e, filename) + if err != nil { + return nil, err + } + if p.config.IsMarkdown(filename) { + b = blackfriday.MarkdownCommon(b) + } + return b, nil +} + +func (p *Pipeline) renderTemplate(b []byte, e templates.VariableMap, filename string) ([]byte, error) { + b, err := p.liquidEngine.ParseAndRender(b, e) + if err != nil { + switch err := err.(type) { + case *liquid.RenderError: + if err.Filename == "" { + err.Filename = filename + } + return nil, err + default: + return nil, helpers.PathError(err, "Liquid Error", filename) + } + } + return b, err +} + +// ApplyLayout applies the named layout to bytes. +func (p *Pipeline) ApplyLayout(name string, b []byte, e templates.VariableMap) ([]byte, error) { + for name != "" { + var lfm templates.VariableMap + t, err := p.FindLayout(name, &lfm) + if err != nil { + return nil, err + } + le := templates.MergeVariableMaps(e, templates.VariableMap{ + "content": string(b), + "layout": lfm, + }) + b, err = t.Render(le) + if err != nil { + return nil, err + } + name = lfm.String("layout", "") + } + return b, nil +} + +func (p *Pipeline) makeLocalLiquidEngine() liquid.Engine { + engine := liquid.NewLocalWrapperEngine() + engine.LinkTagHandler(p.pageSupplier.RelativeFilenameToURL) + includeHandler := func(name string, w io.Writer, scope map[string]interface{}) error { + filename := filepath.Join(p.sourceDir, p.config.IncludesDir, name) + template, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + text, err := engine.ParseAndRender(template, scope) + if err != nil { + return err + } + _, err = w.Write(text) + return err + } + engine.IncludeHandler(includeHandler) + return engine +} + +func (p *Pipeline) makeLiquidClient() (engine liquid.RemoteEngine, err error) { + engine, err = liquid.NewRPCClientEngine(liquid.DefaultServer) + if err != nil { + return + } + urls := map[string]string{} + for _, page := range p.pageSupplier.Pages() { + urls[page.SiteRelPath()] = page.Permalink() + } + err = engine.FileURLMap(urls) + if err != nil { + return + } + err = engine.IncludeDirs([]string{filepath.Join(p.sourceDir, p.config.IncludesDir)}) + return +} + +func (p *Pipeline) makeLiquidEngine(o PipelineOptions) (liquid.Engine, error) { + if o.UseRemoteLiquidEngine { + return p.makeLiquidClient() + } + return p.makeLocalLiquidEngine(), nil +} diff --git a/sites/render.go b/sites/render.go deleted file mode 100644 index 21749e3..0000000 --- a/sites/render.go +++ /dev/null @@ -1,137 +0,0 @@ -package sites - -import ( - "io" - "io/ioutil" - "path/filepath" - - "github.com/osteele/gojekyll/helpers" - "github.com/osteele/gojekyll/liquid" - "github.com/osteele/gojekyll/pages" - "github.com/osteele/gojekyll/templates" - "github.com/russross/blackfriday" -) - -// RenderingContext returns the page rendering context for a container. -func (s *Site) RenderingContext() pages.RenderingContext { - return s -} - -// RenderingPipeline returns the page's rendering context. -func (s *Site) RenderingPipeline() pages.RenderingPipeline { - return s -} - -// OutputExt returns the output extension. -func (s *Site) OutputExt(pathname string) string { - switch { - case s.IsMarkdown(pathname): - return ".html" - case s.IsSassPath(pathname): - return ".css" - default: - return filepath.Ext(pathname) - } -} - -// Render returns nil iff it wrote to the writer -func (s *Site) Render(w io.Writer, b []byte, filename string, e templates.VariableMap) ([]byte, error) { - if s.IsSassPath(filename) { - return nil, s.WriteSass(w, b) - } - b, err := s.renderTemplate(b, e, filename) - if err != nil { - return nil, err - } - if s.IsMarkdown(filename) { - b = blackfriday.MarkdownCommon(b) - } - return b, nil -} - -func (s *Site) renderTemplate(b []byte, e templates.VariableMap, filename string) ([]byte, error) { - b, err := s.liquidEngine.ParseAndRender(b, e) - if err != nil { - switch err := err.(type) { - case *liquid.RenderError: - if err.Filename == "" { - err.Filename = filename - } - return nil, err - default: - return nil, helpers.PathError(err, "Liquid Error", filename) - } - } - return b, err -} - -// ApplyLayout applies the named layout to bytes. -func (s *Site) ApplyLayout(name string, b []byte, e templates.VariableMap) ([]byte, error) { - for name != "" { - var lfm templates.VariableMap - t, err := s.FindLayout(name, &lfm) - if err != nil { - return nil, err - } - le := templates.MergeVariableMaps(e, templates.VariableMap{ - "content": string(b), - "layout": lfm, - }) - b, err = t.Render(le) - if err != nil { - return nil, err - } - name = lfm.String("layout", "") - } - return b, nil -} - -func (s *Site) makeLocalLiquidEngine() liquid.Engine { - engine := liquid.NewLocalWrapperEngine() - engine.LinkTagHandler(s.RelPathURL) - includeHandler := func(name string, w io.Writer, scope map[string]interface{}) error { - filename := filepath.Join(s.Source, s.config.IncludesDir, name) - template, err := ioutil.ReadFile(filename) - if err != nil { - return err - } - text, err := engine.ParseAndRender(template, scope) - if err != nil { - return err - } - _, err = w.Write(text) - return err - } - engine.IncludeHandler(includeHandler) - return engine -} - -func (s *Site) makeLiquidClient() (engine liquid.RemoteEngine, err error) { - engine, err = liquid.NewRPCClientEngine(liquid.DefaultServer) - if err != nil { - return - } - urls := map[string]string{} - for _, p := range s.Paths { - urls[p.SiteRelPath()] = p.Permalink() - } - err = engine.FileURLMap(urls) - if err != nil { - return - } - err = engine.IncludeDirs([]string{filepath.Join(s.Source, s.config.IncludesDir)}) - return -} - -func (s *Site) makeLiquidEngine() (liquid.Engine, error) { - if s.UseRemoteLiquidEngine { - return s.makeLiquidClient() - } - return s.makeLocalLiquidEngine(), nil -} - -// TemplateEngine creates a liquid engine configured to with include paths and link tag resolution -// for this site. -func (s *Site) TemplateEngine() liquid.Engine { - return s.liquidEngine -} diff --git a/sites/sass.go b/sites/sass.go index c1dc1ef..a9555bb 100644 --- a/sites/sass.go +++ b/sites/sass.go @@ -14,26 +14,21 @@ import ( libsass "github.com/wellington/go-libsass" ) -// IsSassPath returns a boolean indicating whether the file is a Sass (".sass" or ".scss") file. -func (s *Site) IsSassPath(name string) bool { - return strings.HasSuffix(name, ".sass") || strings.HasSuffix(name, ".scss") -} - // CopySassFileIncludes copies sass partials into a temporary directory, // removing initial underscores. // TODO delete the temp directory when done -func (s *Site) CopySassFileIncludes() error { +func (p *Pipeline) CopySassFileIncludes() error { // TODO use libsass.ImportsOption instead? - if s.sassTempDir == "" { + if p.sassTempDir == "" { dir, err := ioutil.TempDir(os.TempDir(), "_sass") if err != nil { return err } - s.sassTempDir = dir + p.sassTempDir = dir } - src := filepath.Join(s.Source, "_sass") - dst := s.sassTempDir + src := filepath.Join(p.sourceDir, "_sass") + dst := p.sassTempDir err := filepath.Walk(src, func(from string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return err @@ -52,17 +47,17 @@ func (s *Site) CopySassFileIncludes() error { } // SassIncludePaths returns an array of sass include directories. -func (s *Site) SassIncludePaths() []string { - return []string{s.sassTempDir} +func (p *Pipeline) SassIncludePaths() []string { + return []string{p.sassTempDir} } // WriteSass converts a SASS file and writes it to w. -func (s *Site) WriteSass(w io.Writer, b []byte) error { +func (p *Pipeline) WriteSass(w io.Writer, b []byte) error { comp, err := libsass.New(w, bytes.NewBuffer(b)) if err != nil { return err } - err = comp.Option(libsass.IncludePaths(s.SassIncludePaths())) + err = comp.Option(libsass.IncludePaths(p.SassIncludePaths())) if err != nil { log.Fatal(err) } diff --git a/sites/site.go b/sites/site.go index 12f4fc7..2af8646 100644 --- a/sites/site.go +++ b/sites/site.go @@ -1,6 +1,7 @@ package sites import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -9,7 +10,6 @@ import ( "github.com/osteele/gojekyll/collections" "github.com/osteele/gojekyll/config" "github.com/osteele/gojekyll/helpers" - "github.com/osteele/gojekyll/liquid" "github.com/osteele/gojekyll/pages" "github.com/osteele/gojekyll/templates" ) @@ -25,10 +25,11 @@ type Site struct { Variables templates.VariableMap Paths map[string]pages.Page // URL path -> Page, only for output pages - config config.Config - liquidEngine liquid.Engine - pages []pages.Page // all pages, output or not - sassTempDir string + config config.Config + pipeline *Pipeline + // liquidEngine liquid.Engine + // sassTempDir string + pages []pages.Page // all pages, output or not } // Pages returns a slice of pages. @@ -80,8 +81,8 @@ func (s *Site) RelPathPage(relpath string) (pages.Page, bool) { return nil, false } -// RelPathURL returns a page's relative URL, give a file path relative to the site source directory. -func (s *Site) RelPathURL(relpath string) (string, bool) { +// RelativePathToURL returns a page's relative URL, give a file path relative to the site source directory. +func (s *Site) RelativeFilenameToURL(relpath string) (string, bool) { var url string p, found := s.RelPathPage(relpath) if found { @@ -90,6 +91,20 @@ func (s *Site) RelPathURL(relpath string) (string, bool) { return url, found } +// RenderingPipeline returns the rendering pipeline. +func (s *Site) RenderingPipeline() pages.RenderingPipeline { + if s.pipeline == nil { + panic(fmt.Errorf("uninitialized rendering pipeline")) + } + return s.pipeline +} + +func (s *Site) InitializeRenderingPipeline() (err error) { + o := PipelineOptions{UseRemoteLiquidEngine: s.UseRemoteLiquidEngine} + s.pipeline, err = NewPipeline(s.Source, s.config, s, o) + return +} + // URLPage returns the page that will be served at URL func (s *Site) URLPage(urlpath string) (p pages.Page, found bool) { p, found = s.Paths[urlpath] @@ -121,8 +136,3 @@ func (s *Site) Exclude(path string) bool { return false } } - -// LayoutsDir returns the path to the layouts directory. -func (s *Site) LayoutsDir() string { - return filepath.Join(s.Source, s.config.LayoutsDir) -} diff --git a/sites/site_test.go b/sites/site_test.go index c59415b..23801b8 100644 --- a/sites/site_test.go +++ b/sites/site_test.go @@ -7,8 +7,7 @@ import ( ) func TestIsMarkdown(t *testing.T) { - site := NewSite() - require.True(t, site.IsMarkdown("name.md")) - require.True(t, site.IsMarkdown("name.markdown")) - require.False(t, site.IsMarkdown("name.html")) + s := NewSite() + require.Equal(t, "", s.PathPrefix()) + require.False(t, s.KeepFile("random")) }