diff --git a/README.md b/README.md index c090f15..28e5d30 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Gojekyll is a clone of the [Jekyll](https://jekyllrb.com) static site generator, First-time install: -1. [Install go](https://golang.org/doc/install#install). On macOS running Homebrew, `brew install go` is easier than the linked instructions. +1. [Install go](https://golang.org/doc/install#install). (On macOS running [Homebrew](https://brew.sh), `brew install go` is easier than the Install instructions on the Go site.) 2. `go get osteele/gojekyll/cmd/gojekyll` 3. To use the `{% highlight %}` tag, you need Pygments. `pip install Pygments`. @@ -36,7 +36,7 @@ Update to the latest version: ```bash gojekyll build # builds the site in the current directory into _site -gojekyll serve # serve the app at http://localhost:4000 +gojekyll serve # serve the app at http://localhost:4000; reload on changes gojekyll help gojekyll help build ``` diff --git a/frontmatter/frontmatter.go b/frontmatter/frontmatter.go index 345f937..d6fa5e0 100644 --- a/frontmatter/frontmatter.go +++ b/frontmatter/frontmatter.go @@ -12,6 +12,7 @@ import ( // FrontMatter wraps a map to provide interface functions type FrontMatter map[string]interface{} +// The first four bytes of a file with front matter. const fmMagic = "---\n" // FileHasFrontMatter returns a bool indicating whether the diff --git a/templates/frontmatter.go b/frontmatter/read.go similarity index 54% rename from templates/frontmatter.go rename to frontmatter/read.go index 63c5ed2..b817ad1 100644 --- a/templates/frontmatter.go +++ b/frontmatter/read.go @@ -1,9 +1,11 @@ -package templates +package frontmatter import ( "bytes" "regexp" + "github.com/osteele/gojekyll/templates" + yaml "gopkg.in/yaml.v2" ) @@ -12,13 +14,14 @@ var ( emptyFontMatterMatcher = regexp.MustCompile(`(?s)^---\n+---\n`) ) -// ReadFrontMatter reads the front matter from a document. -func ReadFrontMatter(sourcePtr *[]byte) (frontMatter VariableMap, err error) { +// Read reads the frontmatter from a document. It modifies srcPtr to point to the +// content after the frontmatter, and sets firstLine to its 1-indexed line number. +func Read(sourcePtr *[]byte, firstLine *int) (frontMatter templates.VariableMap, err error) { var ( source = *sourcePtr start = 0 ) - // Replace Windows linefeeds. This allows the following regular expressions to work. + // Replace Windows line feeds. This allows the following regular expressions to work. source = bytes.Replace(source, []byte("\r\n"), []byte("\n"), -1) if match := frontMatterMatcher.FindSubmatchIndex(source); match != nil { start = match[1] @@ -28,10 +31,9 @@ func ReadFrontMatter(sourcePtr *[]byte) (frontMatter VariableMap, err error) { } else if match := emptyFontMatterMatcher.FindSubmatchIndex(source); match != nil { start = match[1] } - // This fixes the line numbers, so that template errors show with the correct line. - // TODO find a less hack-ey solution - *sourcePtr = append( - regexp.MustCompile(`[^\n\r]+`).ReplaceAllLiteral(source[:start], []byte{}), - source[start:]...) + if firstLine != nil { + *firstLine = 1 + bytes.Count(source[:start], []byte("\n")) + } + *sourcePtr = source[start:] return } diff --git a/pages/page.go b/pages/page.go index 9f7f26c..67b0fe1 100644 --- a/pages/page.go +++ b/pages/page.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "time" + "github.com/osteele/gojekyll/frontmatter" "github.com/osteele/gojekyll/templates" "github.com/osteele/liquid/evaluator" ) @@ -27,8 +28,9 @@ type Page interface { type page struct { file - raw []byte - content *[]byte + firstLine int + raw []byte + content *[]byte } // Static is in the File interface. @@ -39,15 +41,16 @@ func makePage(filename string, f file) (*page, error) { if err != nil { return nil, err } - - frontMatter, err := templates.ReadFrontMatter(&b) + lineNo := 1 + frontMatter, err := frontmatter.Read(&b, &lineNo) if err != nil { return nil, err } f.frontMatter = templates.MergeVariableMaps(f.frontMatter, frontMatter) p := page{ - file: f, - raw: b, + file: f, + firstLine: lineNo, + raw: b, } if err = p.setPermalink(); err != nil { return nil, err @@ -106,7 +109,7 @@ func (p *page) Content(rc RenderingContext) ([]byte, error) { if p.content == nil { rp := rc.RenderingPipeline() buf := new(bytes.Buffer) - b, err := rp.Render(buf, p.raw, p.filename, p.TemplateContext(rc)) + b, err := rp.Render(buf, p.raw, p.filename, p.firstLine, p.TemplateContext(rc)) if err != nil { return nil, err } diff --git a/pages/page_test.go b/pages/page_test.go index c1c69ec..4cc2d56 100644 --- a/pages/page_test.go +++ b/pages/page_test.go @@ -17,14 +17,14 @@ type renderingContextFake struct { func (c renderingContextFake) RenderingPipeline() pipelines.PipelineInterface { return c } func (c renderingContextFake) Config() config.Config { return c.cfg } -func (c renderingContextFake) PathPrefix() string { return "." } +func (c renderingContextFake) PathPrefix() string { return "." } func (c renderingContextFake) OutputExt(string) string { return ".html" } func (c renderingContextFake) Site() interface{} { return nil } func (c renderingContextFake) ApplyLayout(layout string, src []byte, vars map[string]interface{}) ([]byte, error) { require.Equal(c.t, "layout1", layout) return nil, nil } -func (c renderingContextFake) Render(w io.Writer, src []byte, filename string, vars map[string]interface{}) ([]byte, error) { +func (c renderingContextFake) Render(w io.Writer, src []byte, filename string, lineNo int, vars map[string]interface{}) ([]byte, error) { require.Equal(c.t, "testdata/page_with_layout.md", filename) return nil, nil } diff --git a/pipelines/layouts.go b/pipelines/layouts.go index e8fad87..740dcf0 100644 --- a/pipelines/layouts.go +++ b/pipelines/layouts.go @@ -7,12 +7,12 @@ import ( "path/filepath" "strings" - "github.com/osteele/gojekyll/templates" + "github.com/osteele/gojekyll/frontmatter" "github.com/osteele/liquid" ) // FindLayout returns a template for the named layout. -func (p *Pipeline) FindLayout(base string, fm *map[string]interface{}) (t liquid.Template, err error) { +func (p *Pipeline) FindLayout(base string, fm *map[string]interface{}) (tpl liquid.Template, err error) { exts := []string{"", ".html"} for _, ext := range strings.SplitN(p.config.MarkdownExt, `,`, -1) { exts = append(exts, "."+ext) @@ -37,15 +37,16 @@ func (p *Pipeline) FindLayout(base string, fm *map[string]interface{}) (t liquid if !found { return nil, fmt.Errorf("no template for %s", base) } - *fm, err = templates.ReadFrontMatter(&content) + lineNo := 1 + *fm, err = frontmatter.Read(&content, &lineNo) if err != nil { return } - t, err = p.liquidEngine.ParseTemplate(content) + tpl, err = p.liquidEngine.ParseTemplate(content) if err != nil { return nil, err } - t.SetSourcePath(filename) + tpl.SetSourceLocation(filename, lineNo) return } diff --git a/pipelines/pipeline.go b/pipelines/pipeline.go index d2fb9ec..1d18cf2 100644 --- a/pipelines/pipeline.go +++ b/pipelines/pipeline.go @@ -16,7 +16,7 @@ import ( type PipelineInterface interface { ApplyLayout(string, []byte, map[string]interface{}) ([]byte, error) OutputExt(pathname string) string - Render(io.Writer, []byte, string, map[string]interface{}) ([]byte, error) + Render(io.Writer, []byte, string, int, map[string]interface{}) ([]byte, error) } // Pipeline applies a rendering transformation to a file. @@ -59,11 +59,11 @@ func (p *Pipeline) OutputExt(pathname string) string { } // Render returns nil iff it wrote to the writer -func (p *Pipeline) Render(w io.Writer, b []byte, filename string, e map[string]interface{}) ([]byte, error) { +func (p *Pipeline) Render(w io.Writer, b []byte, filename string, lineNo int, e map[string]interface{}) ([]byte, error) { if p.config.IsSASSPath(filename) { return nil, p.WriteSass(w, b) } - b, err := p.renderTemplate(b, e, filename) + b, err := p.renderTemplate(b, e, filename, lineNo) if err != nil { return nil, err } @@ -73,12 +73,12 @@ func (p *Pipeline) Render(w io.Writer, b []byte, filename string, e map[string]i return b, nil } -func (p *Pipeline) renderTemplate(src []byte, b map[string]interface{}, filename string) ([]byte, error) { +func (p *Pipeline) renderTemplate(src []byte, b map[string]interface{}, filename string, lineNo int) ([]byte, error) { tpl, err := p.liquidEngine.ParseTemplate(src) if err != nil { return nil, helpers.PathError(err, "Liquid Error", filename) } - tpl.SetSourcePath(filename) + tpl.SetSourceLocation(filename, lineNo) out, err := tpl.Render(b) if err != nil { return nil, helpers.PathError(err, "Liquid Error", filename) @@ -90,7 +90,7 @@ func (p *Pipeline) renderTemplate(src []byte, b map[string]interface{}, filename func (p *Pipeline) ApplyLayout(name string, data []byte, e map[string]interface{}) ([]byte, error) { for name != "" { var lfm map[string]interface{} - t, err := p.FindLayout(name, &lfm) + tpl, err := p.FindLayout(name, &lfm) if err != nil { return nil, err } @@ -98,7 +98,7 @@ func (p *Pipeline) ApplyLayout(name string, data []byte, e map[string]interface{ "content": string(data), "layout": lfm, }) - data, err = t.Render(b) + data, err = tpl.Render(b) if err != nil { return nil, helpers.PathError(err, "render template", name) }