Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions cmd/scriggo/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
127 changes: 103 additions & 24 deletions cmd/scriggo/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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, `<script>
// Scriggo LiveReload.
(function () {
if (typeof EventSource !== 'function') return;
const es = new EventSource('`)
jsStringEscape(w, r.URL.Path)
_, _ = io.WriteString(w, `');
es.onmessage = function(e) {
if (e.data === 'reload') {
es.close();
location.reload();
if srv.liveReloads == nil {
_, _ = b.WriteTo(w)
} else {
s := b.Bytes()
i := indexEndBody(s)
if i == -1 {
i = len(s)
}
};
})();
</script>`)
_, _ = w.Write(s[i:])
_, _ = w.Write(s[:i])
_, _ = writeLiveReloadScript(w, r.URL.Path)
_, _ = w.Write(s[i:])
}
}

if srv.metrics.active {
Expand Down Expand Up @@ -513,3 +514,81 @@ func isBodyTag(s []byte) bool {
}
return false
}

// writeWrappedBody writes a complete HTML page to w. It inserts body between
// the <body> 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, `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>`)
_, _ = io.WriteString(w, title)
_, _ = io.WriteString(w, `</title>
<style>
body {
margin: 0 auto;
max-width: 70ch;
padding: 0 16px;
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
}
body > :first-child {
margin-top: 16px;
}
h1,h2,h3 { line-height: 1.25; margin-top: 1.4em; }
pre {
background: #f3f4f6;
padding: 1rem;
overflow: auto;
border-radius: 8px;
}
code {
background: #f3f4f6;
padding: 0.2em 0.4em;
border-radius: 4px;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ddd;
padding: 0.5rem;
}
</style>
</head>
<body>
`)
_, _ = body.WriteTo(w)
if urlPath != "" {
_, _ = writeLiveReloadScript(w, urlPath)
}
_, _ = io.WriteString(w, `
</body>
</html>`)
}

// 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, `<script>
// Scriggo LiveReload.
(function () {
if (typeof EventSource !== 'function') return;
const es = new EventSource('`)

jsStringEscape(w, urlPath)
_, _ = io.WriteString(w, `');
es.onmessage = function(e) {
if (e.data === 'reload') {
es.close();
location.reload();
}
};
})();
</script>`)
return n, err
}
1 change: 1 addition & 0 deletions internal/compiler/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion internal/runtime/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
73 changes: 73 additions & 0 deletions templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package scriggo

import (
"fmt"
"io/fs"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -190,6 +191,78 @@ func TestFormatFS(t *testing.T) {
}
}

func TestTemplateMainFormatWithExtends(t *testing.T) {
fsys := fstest.Files{
"index.md": `{% extends "layout.html" %}`,
"layout.html": `<h1>Title</h1>`,
}
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": `<h1>hello</h1>`},
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": `<h1>hello</h1>`,
},
name: "index.md",
expected: FormatHTML,
},
"MultipleExtendsUseFinalFormat": {
fsys: fstest.Files{
"index.md": `{% extends "level1.html" %}`,
"level1.html": `{% extends "level2.html" %}`,
"level2.html": `<h1>hello</h1>`,
},
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{
Expand Down
14 changes: 14 additions & 0 deletions test/misc/multi_file_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}`,
Expand Down