diff --git a/commands/commands.go b/commands/commands.go index 8446c71..d4a0c20 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -49,3 +49,25 @@ func Prompt(label string) string { } return strings.TrimSpace(s) } + +type Meta struct { + Expression string `arg:"" name:"expression" default:"site" help:"liquid expression to be evaluated (what goes inside of {{ ... }} in templates)"` +} + +// Load the site metadata and use it as context to evaluate a liquid expression +func (cmd *Meta) Run(ctx *kong.Context) error { + + config, err := config.Load(".") + if err != nil { + return err + } + + // remove optional {{}} wrapper + expression := strings.Trim(cmd.Expression, " {}") + + result, err := site.EvalMetadata(*config, expression) + if err == nil { + fmt.Println(result) + } + return err +} diff --git a/main.go b/main.go index 898d5de..880cff6 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ var cli struct { Build commands.Build `cmd:"" help:"Build a website project." aliases:"b"` Post commands.Post `cmd:"" help:"Initialize a new post template file." aliases:"p"` Serve commands.Serve `cmd:"" help:"Run a local server for the website." aliases:"s"` + Meta commands.Meta `cmd:"" help:"Get the JSON results from evaluating a liquid template expression within the site context." aliases:"m"` Version kong.VersionFlag `short:"v"` } diff --git a/markup/templates.go b/markup/templates.go index 09e1aa2..32e77dc 100644 --- a/markup/templates.go +++ b/markup/templates.go @@ -42,6 +42,11 @@ func NewEngine(siteUrl string, includesDir string) *Engine { return e } +func EvalExpression(engine *Engine, expression string, context map[string]interface{}) (string, error) { + template := fmt.Sprintf("{{ %s | json }}", expression) + return engine.ParseAndRenderString(template, context) +} + // Try to parse a liquid template at the given location. // Files starting with front matter (--- sorrrounded yaml) // are considered templates. If the given file is not headed by front matter diff --git a/site/site.go b/site/site.go index ddd783c..0229a4f 100644 --- a/site/site.go +++ b/site/site.go @@ -49,6 +49,16 @@ func Build(config config.Config) error { return site.build() } +// Parse and render the given liquid expression, eg. " site.posts | map:title " +// and return the results as a json string. +func EvalMetadata(config config.Config, expression string) (string, error) { + site, err := load(config) + if err != nil { + return "", err + } + return markup.EvalExpression(site.templateEngine, expression, site.AsContext()) +} + // Create a new site instance by scanning the project directories // pointed by `config`, loading layouts, templates and data files. func load(config config.Config) (*site, error) { @@ -374,16 +384,7 @@ func (site *site) buildFile(path string) error { } func (site *site) render(templ *markup.Template) ([]byte, error) { - ctx := map[string]interface{}{ - "site": map[string]interface{}{ - "config": site.config.AsContext(), - "posts": site.posts, - "tags": site.tags, - "pages": site.pages, - "static_files": site.static_files, - "data": site.data, - }, - } + ctx := site.AsContext() ctx["page"] = templ.Metadata content, err := templ.RenderWith(ctx, site.config.HighlightTheme) @@ -410,6 +411,19 @@ func (site *site) render(templ *markup.Template) ([]byte, error) { return content, nil } +func (site *site) AsContext() map[string]interface{} { + return map[string]interface{}{ + "site": map[string]interface{}{ + "config": site.config.AsContext(), + "posts": site.posts, + "tags": site.tags, + "pages": site.pages, + "static_files": site.static_files, + "data": site.data, + }, + } +} + func checkFileError(err error) error { // When walking the source dir it can happen that a file is present when walking starts // but missing or inaccessible when trying to open it (this is particularly frequent with diff --git a/site/site_test.go b/site/site_test.go index d7bbcd8..417ffdd 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -271,6 +271,42 @@ date: 2023-01-01 `) } +func TestEvalMetadata(t *testing.T) { + config := newProject() + defer os.RemoveAll(config.RootDir) + + content := `--- +title: hello world! +date: 2024-01-01 +--- +

Hello world!

` + file := newFile(config.SrcDir, "hello.html", content) + defer os.Remove(file.Name()) + + content = `--- +title: goodbye! +date: 2024-02-01 +--- +

goodbye world!

` + file = newFile(config.SrcDir, "goodbye.html", content) + defer os.Remove(file.Name()) + + content = `--- +title: an oldie! +date: 2023-01-01 +--- +

oldie

` + file = newFile(config.SrcDir, "an-oldie.html", content) + defer os.Remove(file.Name()) + + output, err := EvalMetadata(*config, "site.posts | map:'title'") + assertEqual(t, err, nil) + assertEqual(t, output, `["goodbye!","hello world!","an oldie!"]`) + + _, err = EvalMetadata(*config, "site.posts | map:'title") + assert(t, strings.Contains(err.Error(), "Liquid error")) +} + func TestRenderTags(t *testing.T) { config := newProject() defer os.RemoveAll(config.RootDir)