Handle views in combo with Ace base templates
authorbep <bjorn.erik.pedersen@gmail.com>
Sun, 29 Mar 2015 19:12:13 +0000 (20:12 +0100)
committerbep <bjorn.erik.pedersen@gmail.com>
Sun, 29 Mar 2015 19:12:13 +0000 (21:12 +0200)
As views looks like a regular template, but doesn't need a base template, we have to look inside it.

Altough really not needed by this commit, reading the full file content into memory just to do a substring search is a waste.
So this commit implements a `ReaderContains` func that in most cases should be much faster than doing an `ioutil.ReadAll` and `bytes.Contains`:

```
benchmark                   old ns/op     new ns/op     delta
BenchmarkReaderContains     78452         20260         -74.18%

benchmark                   old allocs     new allocs     delta
BenchmarkReaderContains     46             20             -56.52%

benchmark                   old bytes     new bytes     delta
BenchmarkReaderContains     46496         1258          -97.29%
```

Fixes #999

helpers/general.go
helpers/general_test.go
helpers/path.go
tpl/template.go

index 3e0a85fbc5d0a12b71bf33b28e514bca9b03338d..baf957d75a20a6a88a98637f8195bc60f1ce9cc5 100644 (file)
@@ -112,6 +112,42 @@ func BytesToReader(in []byte) io.Reader {
        return bytes.NewReader(in)
 }
 
+// ReaderContains reports whether subslice is within r.
+func ReaderContains(r io.Reader, subslice []byte) bool {
+
+       if len(subslice) == 0 {
+               return false
+       }
+
+       bufflen := len(subslice) * 4
+       halflen := bufflen / 2
+       buff := make([]byte, bufflen)
+       var err error
+       var n, i int
+
+       for {
+               i++
+               if i == 1 {
+                       n, err = io.ReadAtLeast(r, buff[:halflen], halflen)
+               } else {
+                       if i != 2 {
+                               // shift left to catch overlapping matches
+                               copy(buff[:], buff[halflen:])
+                       }
+                       n, err = io.ReadAtLeast(r, buff[halflen:], halflen)
+               }
+
+               if n > 0 && bytes.Contains(buff, subslice) {
+                       return true
+               }
+
+               if err != nil {
+                       break
+               }
+       }
+       return false
+}
+
 // ThemeSet checks whether a theme is in use or not.
 func ThemeSet() bool {
        return viper.GetString("theme") != ""
index b5706a44537c747d8dd3bf2d9b2bc0b748f70a52..496439db12d54ca69d6784ecbbca7d6c870d009d 100644 (file)
@@ -1,7 +1,9 @@
 package helpers
 
 import (
+       "bytes"
        "github.com/stretchr/testify/assert"
+       "io/ioutil"
        "reflect"
        "strings"
        "testing"
@@ -44,6 +46,101 @@ func TestStringToReader(t *testing.T) {
        assert.Equal(t, asString, ReaderToString(asReader))
 }
 
+var containsTestText = (`На берегу пустынных волн
+Стоял он, дум великих полн,
+И вдаль глядел. Пред ним широко
+Река неслася; бедный чёлн
+По ней стремился одиноко.
+По мшистым, топким берегам
+Чернели избы здесь и там,
+Приют убогого чухонца;
+И лес, неведомый лучам
+В тумане спрятанного солнца,
+Кругом шумел.
+
+Τη γλώσσα μου έδωσαν ελληνική
+το σπίτι φτωχικό στις αμμουδιές του Ομήρου.
+Μονάχη έγνοια η γλώσσα μου στις αμμουδιές του Ομήρου.
+
+από το Άξιον Εστί
+του Οδυσσέα Ελύτη
+
+Sîne klâwen durh die wolken sint geslagen,
+er stîget ûf mit grôzer kraft,
+ich sih in grâwen tägelîch als er wil tagen,
+den tac, der im geselleschaft
+erwenden wil, dem werden man,
+den ich mit sorgen în verliez.
+ich bringe in hinnen, ob ich kan.
+sîn vil manegiu tugent michz leisten hiez.
+`)
+
+var containsBenchTestData = []struct {
+       v1     string
+       v2     []byte
+       expect bool
+}{
+       {"abc", []byte("a"), true},
+       {"abc", []byte("b"), true},
+       {"abcdefg", []byte("efg"), true},
+       {"abc", []byte("d"), false},
+       {containsTestText, []byte("стремился"), true},
+       {containsTestText, []byte(containsTestText[10:80]), true},
+       {containsTestText, []byte(containsTestText[100:110]), true},
+       {containsTestText, []byte(containsTestText[len(containsTestText)-100 : len(containsTestText)-10]), true},
+       {containsTestText, []byte(containsTestText[len(containsTestText)-20:]), true},
+       {containsTestText, []byte("notfound"), false},
+}
+
+// some corner cases
+var containsAdditionalTestData = []struct {
+       v1     string
+       v2     []byte
+       expect bool
+}{
+       {"", []byte("a"), false},
+       {"a", []byte(""), false},
+       {"", []byte(""), false},
+}
+
+func TestReaderContains(t *testing.T) {
+       for i, this := range append(containsBenchTestData, containsAdditionalTestData...) {
+               result := ReaderContains(StringToReader(this.v1), this.v2)
+               if result != this.expect {
+                       t.Errorf("[%d] Got %t but expected %t", i, result, this.expect)
+               }
+       }
+}
+
+func BenchmarkReaderContains(b *testing.B) {
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               for i, this := range containsBenchTestData {
+                       result := ReaderContains(StringToReader(this.v1), this.v2)
+                       if result != this.expect {
+                               b.Errorf("[%d] Got %t but expected %t", i, result, this.expect)
+                       }
+               }
+       }
+}
+
+// kept to compare the above
+func _BenchmarkReaderContains(b *testing.B) {
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               for i, this := range containsBenchTestData {
+                       bs, err := ioutil.ReadAll(StringToReader(this.v1))
+                       if err != nil {
+                               b.Fatalf("Failed %s", err)
+                       }
+                       result := bytes.Contains(bs, this.v2)
+                       if result != this.expect {
+                               b.Errorf("[%d] Got %t but expected %t", i, result, this.expect)
+                       }
+               }
+       }
+}
+
 func TestFindAvailablePort(t *testing.T) {
        addr, err := FindAvailablePort()
        assert.Nil(t, err)
index 91419e0c97d425795bc1e92113938df21a270603..546f23d36bb88f390849470040dd07e7c66f85d7 100644 (file)
@@ -16,16 +16,15 @@ package helpers
 import (
        "errors"
        "fmt"
+       "github.com/spf13/afero"
+       jww "github.com/spf13/jwalterweatherman"
+       "github.com/spf13/viper"
        "io"
        "os"
        "path/filepath"
        "regexp"
        "strings"
        "unicode"
-
-       "github.com/spf13/afero"
-       jww "github.com/spf13/jwalterweatherman"
-       "github.com/spf13/viper"
 )
 
 // FilepathPathBridge is a bridge for common functionality in filepath vs path
@@ -153,6 +152,17 @@ func IsEmpty(path string, fs afero.Fs) (bool, error) {
        return fi.Size() == 0, nil
 }
 
+// Check if a file contains a specified string.
+func FileContains(filename string, subslice []byte, fs afero.Fs) (bool, error) {
+       f, err := os.Open(filename)
+       if err != nil {
+               return false, err
+       }
+       defer f.Close()
+
+       return ReaderContains(f, subslice), nil
+}
+
 // Check if a file or directory exists.
 func Exists(path string, fs afero.Fs) (bool, error) {
        _, err := fs.Stat(path)
index 171d61825eba2b776a59ba71fdc637b7b3860fa8..2db7c4f79860c00c6db99c2054c6c74656556cdc 100644 (file)
@@ -1349,6 +1349,8 @@ func isBackupFile(path string) bool {
 
 const baseAceFilename = "baseof.ace"
 
+var aceTemplateInnerMarker = []byte("= content")
+
 func isBaseTemplate(path string) bool {
        return strings.HasSuffix(path, baseAceFilename)
 }
@@ -1391,14 +1393,22 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
 
                        // ACE templates may have both a base and inner template.
                        if filepath.Ext(path) == ".ace" && !strings.HasSuffix(filepath.Dir(path), "partials") {
-                               // Look for the base first in the current path, then in _default.
-                               p := filepath.Join(filepath.Dir(path), baseAceFilename)
-                               if ok, err := helpers.Exists(p, hugofs.OsFs); err == nil && ok {
-                                       baseTemplatePath = p
-                               } else {
-                                       p := filepath.Join(absPath, "_default", baseAceFilename)
+                               // This may be a view that shouldn't have base template
+                               // Have to look inside it to make sure
+                               needsBase, err := helpers.FileContains(path, aceTemplateInnerMarker, hugofs.OsFs)
+                               if err != nil {
+                                       return err
+                               }
+                               if needsBase {
+                                       // Look for the base first in the current path, then in _default.
+                                       p := filepath.Join(filepath.Dir(path), baseAceFilename)
                                        if ok, err := helpers.Exists(p, hugofs.OsFs); err == nil && ok {
                                                baseTemplatePath = p
+                                       } else {
+                                               p := filepath.Join(absPath, "_default", baseAceFilename)
+                                               if ok, err := helpers.Exists(p, hugofs.OsFs); err == nil && ok {
+                                                       baseTemplatePath = p
+                                               }
                                        }
                                }
                        }