Skip to content

Commit

Permalink
feat: added component JavaScript support, see #23
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h committed Oct 10, 2021
1 parent 4eb4878 commit 3784d4e
Show file tree
Hide file tree
Showing 25 changed files with 816 additions and 50 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,27 @@ Then call it in a template. So long as the `Raw` function is in scope, you can u
{%! Raw("<script>alert('xss vector');</script>") %}
```

For larger scripts you want to embed, you should create a code component that writes the constant to the output writer using the embed feature of Go - see https://pkg.go.dev/embed for more information.

```
func EmbeddedScript(s string) Component {
return ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
_, err = io.WriteString(w, "<script>")
if err != nil {
return
}
//go:embed script.js
var b []byte
_, err = w.Write(b)
if err != nil {
return
}
_, err = io.WriteString(w, "</script>")
return
})
}
```

### Elements

HTML elements look like HTML and you can write static attributes into them, just like with normal HTML. Don't worry about the spacing, the HTML will be minified when it's rendered.
Expand Down
2 changes: 2 additions & 0 deletions example/posts_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

159 changes: 143 additions & 16 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ func (g *generator) writeTemplateNodes() error {
if err := g.writeCSS(n); err != nil {
return err
}
case parser.ScriptTemplate:
if err := g.writeScript(n); err != nil {
return err
}
default:
return fmt.Errorf("unknown node type: %v", reflect.TypeOf(n))
}
Expand Down Expand Up @@ -300,6 +304,10 @@ func (g *generator) writeTemplate(t parser.HTMLTemplate) error {
if _, err = g.w.WriteIndent(indentLevel, "ctx, _ = templ.RenderedCSSClassesFromContext(ctx)\n"); err != nil {
return err
}
// ctx, _ = templ.RenderedScriptsFromContext(ctx)
if _, err = g.w.WriteIndent(indentLevel, "ctx, _ = templ.RenderedScriptsFromContext(ctx)\n"); err != nil {
return err
}
// Nodes.
if err = g.writeNodes(indentLevel, t.Children); err != nil {
return err
Expand Down Expand Up @@ -573,10 +581,14 @@ func (g *generator) writeStandardElement(indentLevel int, n parser.Element) (err
return err
}
} else {
// <style type="text/css></style>
// <style type="text/css"></style>
if err = g.writeElementCSS(indentLevel, n); err != nil {
return err
}
// <script type="text/javascript"></script>
if err = g.writeElementScript(indentLevel, n); err != nil {
return err
}
// <div
if _, err = g.w.WriteIndent(indentLevel, fmt.Sprintf(`_, err = io.WriteString(w, "<%s")`+"\n", html.EscapeString(n.Name))); err != nil {
return err
Expand Down Expand Up @@ -647,6 +659,27 @@ func (g *generator) writeElementCSS(indentLevel int, n parser.Element) (err erro
return err
}

func (g *generator) writeElementScript(indentLevel int, n parser.Element) (err error) {
var scriptExpressions []string
for i := 0; i < len(n.Attributes); i++ {
if attr, ok := n.Attributes[i].(parser.ExpressionAttribute); ok {
name := html.EscapeString(attr.Name)
if strings.HasPrefix(name, "on") {
scriptExpressions = append(scriptExpressions, attr.Expression.Value)
}
}
}
// Render the scripts before the element if required.
// err = templ.RenderScripts(ctx, w, a, b, c)
if _, err = g.w.WriteIndent(indentLevel, "err = templ.RenderScripts(ctx, w, "+strings.Join(scriptExpressions, ", ")+")\n"); err != nil {
return err
}
if err = g.writeErrorHandler(indentLevel); err != nil {
return err
}
return err
}

func (g *generator) writeElementAttributes(indentLevel int, n parser.Element) (err error) {
var r parser.Range
for i := 0; i < len(n.Attributes); i++ {
Expand Down Expand Up @@ -735,21 +768,44 @@ func (g *generator) writeElementAttributes(indentLevel int, n parser.Element) (e
return err
}
} else {
// io.WriteString(w, templ.EscapeString(
if _, err = g.w.WriteIndent(indentLevel, "_, err = io.WriteString(w, templ.EscapeString("); err != nil {
return err
}
// p.Name()
if r, err = g.w.Write(attr.Expression.Value); err != nil {
return err
}
g.sourceMap.Add(attr.Expression, r)
// ))
if _, err = g.w.Write("))\n"); err != nil {
return err
}
if err = g.writeErrorHandler(indentLevel); err != nil {
return err
if strings.HasPrefix(attr.Name, "on") {
// It's a JavaScript handler, and requires special handling, because we expect a JavaScript expression.
vn := g.createVariableName()
// var vn templ.ComponentScript =
if _, err = g.w.WriteIndent(indentLevel, "var "+vn+" templ.ComponentScript = "); err != nil {
return err
}
// p.Name()
if r, err = g.w.Write(attr.Expression.Value); err != nil {
return err
}
g.sourceMap.Add(attr.Expression, r)
if _, err = g.w.Write("\n"); err != nil {
return err
}
if _, err = g.w.WriteIndent(indentLevel, "_, err = io.WriteString(w, "+vn+".Call)\n"); err != nil {
return err
}
if err = g.writeErrorHandler(indentLevel); err != nil {
return err
}
} else {
// io.WriteString(w, templ.EscapeString(
if _, err = g.w.WriteIndent(indentLevel, "_, err = io.WriteString(w, templ.EscapeString("); err != nil {
return err
}
// p.Name()
if r, err = g.w.Write(attr.Expression.Value); err != nil {
return err
}
g.sourceMap.Add(attr.Expression, r)
// ))
if _, err = g.w.Write("))\n"); err != nil {
return err
}
if err = g.writeErrorHandler(indentLevel); err != nil {
return err
}
}
}
// Close quote.
Expand Down Expand Up @@ -822,3 +878,74 @@ func createGoString(s string) string {
sb.WriteRune('`')
return sb.String()
}

func (g *generator) writeScript(t parser.ScriptTemplate) error {
var r parser.Range
var err error
var indentLevel int

// func
if _, err = g.w.Write("func "); err != nil {
return err
}
if r, err = g.w.Write(t.Name.Value); err != nil {
return err
}
g.sourceMap.Add(t.Name, r)
// (
if _, err = g.w.Write("("); err != nil {
return err
}
// Write parameters.
if r, err = g.w.Write(t.Parameters.Value); err != nil {
return err
}
g.sourceMap.Add(t.Parameters, r)
// ) templ.ComponentScript {
if _, err = g.w.Write(") templ.ComponentScript {\n"); err != nil {
return err
}
indentLevel++
// return templ.ComponentScript{
if _, err = g.w.WriteIndent(indentLevel, "return templ.ComponentScript{\n"); err != nil {
return err
}
{
indentLevel++
// Name: "scriptName",
if _, err = g.w.WriteIndent(indentLevel, "Name: "+createGoString(t.Name.Value)+",\n"); err != nil {
return err
}
// Function: `function scriptName(a, b, c){` + `constantScriptValue` + `}`,
prefix := "function " + t.Name.Value + "(" + stripTypes(t.Parameters.Value) + "){"
suffix := "}"
if _, err = g.w.WriteIndent(indentLevel, "Function: "+createGoString(prefix+strings.TrimSpace(t.Value)+suffix)+",\n"); err != nil {
return err
}
// Call: templ.SafeScript(scriptName, a, b, c)
if _, err = g.w.WriteIndent(indentLevel, "Call: templ.SafeScript("+createGoString(t.Name.Value)+", "+stripTypes(t.Parameters.Value)+"),\n"); err != nil {
return err
}
indentLevel--
}
// }
if _, err = g.w.WriteIndent(indentLevel, "}\n"); err != nil {
return err
}
indentLevel--
// }
if _, err = g.w.WriteIndent(indentLevel, "}\n\n"); err != nil {
return err
}
return nil
}

func stripTypes(parameters string) string {
variableNames := []string{}
params := strings.Split(parameters, ",")
for i := 0; i < len(params); i++ {
p := strings.Split(strings.TrimSpace(params[i]), " ")
variableNames = append(variableNames, strings.TrimSpace(p[0]))
}
return strings.Join(variableNames, ", ")
}
13 changes: 13 additions & 0 deletions generator/test-a-href/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions generator/test-attribute-escaping/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions generator/test-call/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions generator/test-css-usage/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions generator/test-doctype/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions generator/test-for/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3784d4e

Please sign in to comment.