diff --git a/cmd/scriggo/help.go b/cmd/scriggo/help.go index 6acd906ee..4653defc8 100644 --- a/cmd/scriggo/help.go +++ b/cmd/scriggo/help.go @@ -269,20 +269,27 @@ usage: scriggo serve [-S n] [--metrics] [--disable-livereload] [-const name=valu Serve runs a web server and serves the template rooted at the current directory. It is useful to learn Scriggo templates. -It renders HTML and Markdown files based on file extension. +Path resolution rules: -For example: + /path/name.html + renders 'path/name.html' as an HTML template. - http://localhost:8080/article + /path/name.md + serves the 'path/name.md' file as-is (without rendering it to HTML). -it renders the file 'article.html' as HTML if exists, otherwise renders the -file 'article.md' as Markdown. + /path/name + renders 'path/name.html' if it exists; otherwise, if 'path/name.md' + exists, renders it as Markdown and wraps it in a full HTML page. -Serving a URL terminating with a slash: + /path/ and /path + render 'path/index' using the previous rule. - http://localhost:8080/blog/ +Example: -it renders 'blog/index.html' or 'blog/index.md'. + http://localhost:8080/article + +it renders the file 'article.html' as HTML if exists, otherwise renders the +file 'article.md' as Markdown wrapped in HTML. Markdown is converted to HTML with the Goldmark parser with the options html.WithUnsafe, parser.WithAutoHeadingID and extension.GFM. diff --git a/cmd/scriggo/serve.go b/cmd/scriggo/serve.go index eb138bd1b..3448be5a7 100644 --- a/cmd/scriggo/serve.go +++ b/cmd/scriggo/serve.go @@ -296,7 +296,7 @@ func (srv *server) serveTemplate(w http.ResponseWriter, r *http.Request) { } return } - } else if ext != ".html" && ext != ".md" { + } else if ext != ".html" { srv.static.ServeHTTP(w, r) return } @@ -366,32 +366,33 @@ func (srv *server) serveTemplate(w http.ResponseWriter, r *http.Request) { } runTime := time.Since(start) - s := b.Bytes() - i := indexEndBody(s) - if i == -1 { - i = len(s) - } w.Header().Set("Content-Type", "text/html; charset=utf-8") - if srv.liveReloads == nil { - _, _ = w.Write(s) + + if template.Format() == scriggo.FormatMarkdown { + md := goldmark.New(goldmarkOptions...) + var body bytes.Buffer + err = md.Convert(b.Bytes(), &body) + if err != nil { + http.Error(w, err.Error(), 500) + } + var urlPath string + if srv.liveReloads != nil { + urlPath = r.URL.Path + } + writeWrappedBody(w, urlPath, name, &body) } else { - _, _ = w.Write(s[:i]) - _, _ = io.WriteString(w, ``) - _, _ = w.Write(s[i:]) + _, _ = w.Write(s[:i]) + _, _ = writeLiveReloadScript(w, r.URL.Path) + _, _ = w.Write(s[i:]) + } } if srv.metrics.active { @@ -513,3 +514,81 @@ func isBodyTag(s []byte) bool { } return false } + +// writeWrappedBody writes a complete HTML page to w. It inserts body between +// the tags. If urlPath is non-empty, it includes a live reload script +// using urlPath. The title parameter specifies the page title. +func writeWrappedBody(w io.Writer, urlPath, title string, body *bytes.Buffer) { + _, _ = io.WriteString(w, ` + + + + + `) + _, _ = io.WriteString(w, title) + _, _ = io.WriteString(w, ` + + + +`) + _, _ = body.WriteTo(w) + if urlPath != "" { + _, _ = writeLiveReloadScript(w, urlPath) + } + _, _ = io.WriteString(w, ` + +`) +} + +// writeLiveReloadScript writes a live reload script for urlPath to w. +// It returns the number of bytes written and any error. +func writeLiveReloadScript(w io.Writer, urlPath string) (int, error) { + n, err := io.WriteString(w, ``) + return n, err +} diff --git a/internal/compiler/checker.go b/internal/compiler/checker.go index 31550fe89..b5af8a26a 100644 --- a/internal/compiler/checker.go +++ b/internal/compiler/checker.go @@ -84,6 +84,7 @@ func typecheck(tree *ast.Tree, importer native.Importer, opts checkerOptions) (m compilation.extendedTrees[extends.Tree.Path] = true tree.Nodes = append([]ast.Node{dummyImport}, extends.Tree.Nodes...) tree.Path = extends.Tree.Path + tree.Format = extends.Tree.Format tc.path = extends.Tree.Path } diff --git a/internal/runtime/run.go b/internal/runtime/run.go index ddf7ec857..f166d1ea4 100644 --- a/internal/runtime/run.go +++ b/internal/runtime/run.go @@ -1585,7 +1585,7 @@ func (vm *VM) run() (Addr, bool) { if b == ReturnString { out := vm.renderer.Out().(*strings.Builder) vm.setString(1, out.String()) - } else if fn.Format == ast.FormatMarkdown && ast.Format(b) == ast.FormatHTML { + } else if vm.fn.Format == ast.FormatMarkdown && ast.Format(b) == ast.FormatHTML { out := vm.renderer.Out().(*bytes.Buffer) err := vm.env.conv(out.Bytes(), call.renderer.out) if err != nil { diff --git a/templates.go b/templates.go index 9dfadb111..55eae3bf5 100644 --- a/templates.go +++ b/templates.go @@ -174,6 +174,11 @@ func (t *Template) Disassemble(n int) []byte { return assemblies["main"] } +// Format returns the output format of the template when executed. +func (t *Template) Format() Format { + return Format(t.fn.Format) +} + // UsedVars returns the names of the global variables used in the template. // A variable used in dead code may not be returned as used. func (t *Template) UsedVars() []string { diff --git a/templates_test.go b/templates_test.go index bbb477fbf..af2830f4a 100644 --- a/templates_test.go +++ b/templates_test.go @@ -6,6 +6,7 @@ package scriggo import ( "fmt" + "io/fs" "reflect" "strings" "testing" @@ -190,6 +191,78 @@ func TestFormatFS(t *testing.T) { } } +func TestTemplateMainFormatWithExtends(t *testing.T) { + fsys := fstest.Files{ + "index.md": `{% extends "layout.html" %}`, + "layout.html": `

Title

`, + } + template, err := BuildTemplate(fsys, "index.md", nil) + if err != nil { + t.Fatal(err) + } + if template.fn.Format != ast.FormatHTML { + t.Fatalf("expected main format %s, got %s", ast.FormatHTML, template.fn.Format) + } +} + +func TestTemplateFormat(t *testing.T) { + tests := map[string]struct { + fsys fs.FS + name string + expected Format + }{ + "Text": { + fsys: fstest.Files{"index.txt": `hello`}, + name: "index.txt", + expected: FormatText, + }, + "HTML": { + fsys: fstest.Files{"index.html": `

hello

`}, + name: "index.html", + expected: FormatHTML, + }, + "Markdown": { + fsys: fstest.Files{"index.md": `# hello`}, + name: "index.md", + expected: FormatMarkdown, + }, + "MarkdownExtendsHTML": { + fsys: fstest.Files{ + "index.md": `{% extends "layout.html" %}`, + "layout.html": `

hello

`, + }, + name: "index.md", + expected: FormatHTML, + }, + "MultipleExtendsUseFinalFormat": { + fsys: fstest.Files{ + "index.md": `{% extends "level1.html" %}`, + "level1.html": `{% extends "level2.html" %}`, + "level2.html": `

hello

`, + }, + name: "index.md", + expected: FormatHTML, + }, + "FormatFS": { + fsys: testFormatFS{Files: fstest.Files{"index": `hello`}, format: FormatJSON}, + name: "index", + expected: FormatJSON, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + template, err := BuildTemplate(tc.fsys, tc.name, nil) + if err != nil { + t.Fatal(err) + } + if got := template.Format(); got != tc.expected { + t.Fatalf("expected %s, got %s", tc.expected, got) + } + }) + } +} + // TestUnexpandedTransformer ensures the transformer runs before expansion. func TestUnexpandedTransformer(t *testing.T) { fsys := fstest.Files{ diff --git a/test/misc/multi_file_template_test.go b/test/misc/multi_file_template_test.go index bbdf860e0..242249c0f 100644 --- a/test/misc/multi_file_template_test.go +++ b/test/misc/multi_file_template_test.go @@ -3256,6 +3256,20 @@ func TestMultiFileTemplate(t *testing.T) { expectedOut: "--- start Markdown ---\n**bold**--- end Markdown ---\n", }, + "Show markdown macro in an HTML context": { + sources: fstest.Files{ + "index.html": `{% macro M markdown %}# Hi{% end %}{{ M() }}`, + }, + expectedOut: "--- start Markdown ---\n# Hi--- end Markdown ---\n", + }, + + "Show markdown macro in an HTML context - Indirect": { + sources: fstest.Files{ + "index.html": `{% macro M markdown %}# Hi{% end %}{% _ = &M %}{{ M() }}`, + }, + expectedOut: "--- start Markdown ---\n# Hi--- end Markdown ---\n", + }, + "Recursive macro call (1)": { sources: fstest.Files{ "index.html": `{% macro m(i int) %}{{ i }}{% if i > 0 %}{{ m(i - 1) }}{% end if %}{% end macro %}{{ m(5) }}`,