Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: change to templ to use single braces #46

Closed
a-h opened this issue Apr 2, 2022 · 13 comments
Closed

proposal: change to templ to use single braces #46

a-h opened this issue Apr 2, 2022 · 13 comments

Comments

@a-h
Copy link
Owner

a-h commented Apr 2, 2022

In #35, there's a suggestion that the templ expression start and end token of {% and %} is unwieldy.

Why did we end up with {% / %}?

Initially, when choosing delimiters I was looking for something that was easy to type, reasonable looking, and not likely to show up in code.

I considered double braces {{ but since the standard library, mustache and hugo all use these, and that double braces show up in Go code, I didn't think it would ideal.

I ended up with the {% idea because it isn't valid Go code.

What would be better?

I think a single brace { would be best, because it's the smallest amount of typing, but thought that the complexity of the parser would be high, since I'd have to parse Go code to find the end of statements.

How?

However, I think I've got a solution to remove the need for {% and to drop it to {.

The templ expressions only show up at particular points in the document (e.g. attribute values, within the main document), there's a strong expectation for the expression to be present, meaning that it's less important for the templ start/end token to be globally unique.

If I use a brace instead, the parser can parse until the closing brace. Braces in Go code must be balanced, so any Go code will balance the brace count, however, the parser would need to ignore braces that are within string literals, since they may contain unbalanced braces.

The basic algorithm is:

  • Start with an expression start e.g. { templ and set braceCount = 1
  • Read char
    • If " handle any escaped quotes or ticks until the end of the string.
    • If ``` read until the closing tick.
    • If { or }
      • If { do braceCount++
      • If } do braceCount--
      • If braceCount == 0, break
    • If EOL, break
    • If EOF, break
    • Default: add the char the expression, goto Read char
  • If braceCount != 0 throw an error

I've written an implementation here which shows how expressions can be parsed.

https://gist.github.com/a-h/c7c2a04afd5605c48496e5b346956455

Difference between current and new code

  • Old: {% if findOut(func() bool { return true }) %}

  • New: { if findOut(func() bool { return true }) }

  • Old: {% endif %}

  • New: { endif }

Since the only allowed expression here is a string expression, there's no need to specify the equals sign {%=, making it possible to simplify down by two chars.

  • Old: <a href={%= "https://google.com" %} />
  • New: <a href={ "https://google.com" } />

The same applies here.

  • Old: <div>{%= theData() %}</div>
  • Old: <div>{ theData() }</div>

However, loading a template would still need a specifier:

  • Old: <div>{%! header() %}</div>
  • New: <div>{! header() }</div>

Backwards compatibility

I don't think it's a good idea to break existing code, so I would:

  • Add a templ migrate function which does find and replace in templ files, replacing:
    • {% with {
    • {%= with {
    • {%! with {!
    • %} with }
  • Update templ generate and templ fmt - if they see the old pattern {% in the code, they would stop and print out a suggestion to run templ migrate, explain the reasoning for the change, and point to this issue.

Ecosystem

The templ vim plugin and VS Code plugin would need to be updated. I don't propose maintaining backwards compatibility for these, since the migration will be one way.

Thoughts / feedback?

Is this worth doing?
Any concerns on this migration?
@joerdav - I assume it's not a big job to migrate the vim plugin if I went ahead with this?

@joerdav
Copy link
Collaborator

joerdav commented Apr 2, 2022

I think this is definitely worth doing, whatever we can to make it easier to use.

Shouldn't be massive to fix the vim plugin.

Had a thought, do the starting blocks and end blocks need brackets at all?

package main

templ name(n string)
    { if n == "" }
        <div>Hello, world</div>
    { else }
        <div>Hello, {n}</div>
    { endif }
endtempl

@a-h
Copy link
Owner Author

a-h commented Apr 2, 2022

Ha, good point!

I need to think about that. If we go far enough down the track, then it stops being HTML with Go code in it, and becomes Go code with HTML in it.

Although I could take some shortcuts - basically, just copy everything outside templ ... and endtempl statements into the output Go file. The LSP would still provide useful stuff.

But then, templ needing endtempl looks weird.

templ name(n string) {
    { if n == "" }
        <div>Hello, world</div>
    { else }
        <div>Hello, {n}</div>
    { endif }
}

But if you remove the endtempl, the endif looks weird and it makes sense to get rid of the extra braces around the expressions...

templ name(n string) {
    if n == "" {
        <div>Hello, world</div>
    } else {
        <div>Hello, {n}</div>
    }
}

And then... templ would basically have to implement the whole of the Go parser, plus some more, which is quite a bit more work.

I've got an idea of how that could be simplified, but I haven't thought it through yet. What do you think of this...

Outside of templ blocks, everything is Go code, and it's copied as-is to the output. The LSP would work fine.

As soon as the parser hits a templ block, the parsing becomes line-oriented.

For each line, the parser has to count the braces (so that we can find the end of the block) and parse strings to avoid mismatches, but each line is just assumed to be Go code and is copied to the output except when the line starts with something templ-y.

So, if the line starts with optional whitespace followed by < (HTML tags), or {! in which case it becomes Go code to call a template.

So you'd write something like this.

package main

func main() {
  // Whatever.
}

templ Name(n string) {
    if n == "" {
        <div>Hello, world</div>
    } else {
        <div>Hello, {n}</div>
    }
   {! otherTemplate(n) } // Becomes `otherTemplate(n).RenderTemplate(ctx, w)`
}

@joerdav
Copy link
Collaborator

joerdav commented Apr 2, 2022

I like that, I especially like that this would make putting the input struct for a template in the same file. Keeping things together.

I think writing HTML inside Go is good, currently it's Go inside HTML inside templ. And we want to make it as familiar as possible which means reducing the amount of templ involved, which I think this gives us.

@a-h
Copy link
Owner Author

a-h commented Apr 3, 2022

Started working on it over at https://github.com/a-h/templ/tree/templ_braces

@a-h
Copy link
Owner Author

a-h commented Apr 3, 2022

Got to work out a nice scheme for conditionals within elements.

Since Go is the default in this scheme, it's not possible to have bare text, because if someone started a sentence with a Go keyword (e.g. if, switch etc.) it would be impossible to tell the difference between Go code and text.

So this would be allowed.

<p>{ "Text" }</p>

Or

<p><t>Text</t></p>

Where t (or <>Text</> is a special element that can't have any Go code in it.

This allows for conditionals in text.

<div>
  <t>Hello, </t>
  if p.Name == "" {
    <t>World!</t>
  } else {
    { p.Name }<t>!</t>
  }
</div>

@aight8
Copy link

aight8 commented Apr 3, 2022

I rember this library:
https://github.com/8byt/gox
(I never tried out because it's not maintaned anymore)

Some time ago I searched jsx like syntax for templating in go. The great strength of next.js (serverside react) is the typed templating possibilities (jsx).

The templ Name(n string) { is also nice (incl all the feature full extra syntax when it make sense). But please also support templ (s Type) Name(n string) { :P

@a-h
Copy link
Owner Author

a-h commented Apr 11, 2022

In the branch, I've got an implementation in place, with all the tests passing with the new proposed syntax, code generator working etc.

templ fmt hasn't been updated in the branch though, it will just break everything at the moment.

package main

import "fmt"
import "time"

templ headerTemplate(name string) {
	<header data-testid="headerTemplate">
		<h1>{ name }</h1>
	</header>
}

templ footerTemplate() {
	<footer data-testid="footerTemplate">
		<div>&copy; { fmt.Sprintf("%d", time.Now().Year()) }</div>
	</footer>
}

templ navTemplate() {
	<nav data-testid="navTemplate">
		<ul>
			<li><a href="/">Home</a></li>
			<li><a href="/posts">Posts</a></li>
		</ul>
	</nav>
}

templ layout(name string, content templ.Component) {
	<html>
		<head><title>{ name }</title></head>
		<body>
			{! headerTemplate(name) }
			{! navTemplate() }
			<main>
				{! content }
			</main>
		</body>
		{! footerTemplate() }
	</html>
}

templ homeTemplate() {
	<div data-testid="homeTemplate">Welcome to my website.</div>
}

templ postsTemplate(posts []Post) {
	<div data-testid="postsTemplate">
		for _, p := range posts {
			<div data-testid="postsTemplatePost">
				<div data-testid="postsTemplatePostName">{ p.Name }</div>
				<div data-testid="postsTemplatePostAuthor">{ p.Author }</div>
			</div>
		}
	</div>
}

templ home() {
	{! layout("Home", homeTemplate()) }
}

templ posts(posts []Post) {
	{! layout("Posts", postsTemplate(posts)) }
}

You'll notice I didn't need to have a <>Text</> element. I used the Go keyword and structure to determine whether the text should be interpreted as HTML text or Go code.

I might need to add an empty element as a get-out clause in case someone wants to start a sentence with if .+ {\n for some reason...

To migrate from existing templates, it would be possible to have something parse the old templates into the object model using the old parser, drop out some JSON, then generate new templates from that.

What do you think of it compared to the existing syntax?

@joerdav
Copy link
Collaborator

joerdav commented Apr 12, 2022

I think it definitely feels more familiar and intuitive.

I think escaping Go code could be solved by supporting multiline strings in some way, for example:

A component that displays how to do an if statement in templ:

templ ifStatement() {
	<div data-testid="ifStatement">
		{`if true {
			<p data-testid="text">
                                some text
			</p>
		}`}
	</div>
}

This then got me thinking about what if I wanted to display an example of how to use this.
It's a bit ugly but also is sort of a convoluted case. But for reference, this is the same as how you would escape back ticks in Go.

templ escapeStatement() {
	<div data-testid="escapeStatement">
                {`
	        <div data-testid="ifStatement">
		        ` + "{`" + `if true {
			        <p data-testid="text">
                                        some text
			        </p>
		        }` + "`}" + `
	        </div>
               `}
	</div>
}

@a-h
Copy link
Owner Author

a-h commented May 2, 2022

I've made some progress on this at https://github.com/a-h/templ/tree/templ_braces

The most important change is that I've added a migration command to migrate from v1 to v2 template files.

Run templ migrate -f ./storybook/example/templates.templ to migrate an individual file, or templ migrate to migrate everything in the current path.

templ fmt now works on V2 files (and will warn to run migrate if V1 files are detected).

Still to complete is the addition of the new <> and </> tags for handling HTML text without if / else etc. being allowed.

templ v2 enables code like this, assuming it's called text.templ:

package main

import "os"

func main() {
    Template().Render(context.Background(), os.Stdout)
}

templ Template() {
   <p>
      This is some text.
      if true {
	      So is this.
      }
   </p>
}

templ generate && go run text_templ.go produces the output of:

<p>This is some text.So is this.</p>

Maybe a bit more thought required about how whitespace is handled, but it's definitely looking right.

@a-h
Copy link
Owner Author

a-h commented May 16, 2022

I dealt with the whitespace problems over the weekend, and I tested a migration on a large project. I think it's looking good for a release.

@joerdav
Copy link
Collaborator

joerdav commented May 18, 2022

Looks perfect, much easier to read I think.
I think the gif needs updating on the README was the only thing I could spot.

@a-h
Copy link
Owner Author

a-h commented May 22, 2022

Today I updated the VS Code extension to support v2 syntax. To try it out, you can open up https://github.com/a-h/templ-vscode in VS Code, and hit F5.

That will cause a new VS Code window to open, configured with the extension. You'll need to have the templ_braces build installed on your path for it to make sense of autocomplete etc. though.

@a-h
Copy link
Owner Author

a-h commented Jun 3, 2022

I've basically rewritten the Language Server, replacing it with https://github.com/go-language-server/protocol because the Sourcegraph implementation was missing anything relating to V3.

While I was doing that, I added a new feature to the generate system that outputs a HTML page to view the mapping between Go code within templ files, and the Go code in the generated code. This allows visualisation of the source maps which power the language server.

image

I think the LSP is much easier to work with now, and it's obvious where to implement functionality (server.go).

I think it's feature complete now, I'm going to do some more testing, then put out a v2.0 tagged release.

a-h added a commit that referenced this issue Jun 5, 2022
* feat: start reworking the parser to support bare Go code

* wip: continuing parser work

* wip: add the if expression back in to support the template parser

* wip: fixed broken template file test

* wip: refactor from functions to static variables

* wip: added support for if statements

* wip: add comments to output to show what was parsed

* wip: reimplement switch statements

* wip: add the if unit tests back in

* wip: update README

* wip: add for loop support back in

* feat: don't render scripts if they are not required

* refactor: fix the formatting tests

* feat: add support for templates to be receivers, see #46 and #35 (item 1)

* refactor: have v1 and v2 side-by-side to enable easier migration

* refactor: continue v1 and v2 side-by-side

* feat: throw an error if a legacy format is encountered

* wip: added start of updated migration function

* fix: manually copy between v1 and v2 instead of using copier

* feat: complete migration

* docs: update for version 2

* fix: broken package test.

* fix: broken reformatting of case statements.

* feat: continue case statement work

* fix: remove additional whitespace

* fix: break on newline to allow inline if

* fix: bug in Windows line end parsing affecting CSS

* feat: add a -w parameter to choose how many cores to use during templ generation

* fix: ensure that the server always closes

* fix: ensure that code can be at the end of files too

* refactor: simplify parsing behaviour to be line oriented

* feat: normalise whitespace

* feat: use the number of CPUs available to the machine

* feat: handle trailing whitespace after case statements

* feat: improve the formatting behaviour

* feat: set tables to format block style

* feat: add style and script elements that ignore the contents

* feat: add textDocument/codeLens support

* refactor: remove TODO

* refactor: copy code from github.com/sourcegraph/go-lsp

* refactor: migrate to go.lsp.dev/protocol

* feat: migrate the LSP server and rewrite functionality to go.lsp.dev

* feat: add sourcemap visualisation

* feat: improve sourcemap rendering

* feat: fix the source location mapping

* refactor: use zero-indexed line indices to match the LSP specification

* security: CVE-2022-28948

* chore: upgrade goquery to 1.8.0

* chore: move example to _example to get Go tools to ignore from the top level

* feat: rewrite the hover output positions

* fix: handle that hover can return nil

* feat(lsp): add Implementation

* feat: improve LSP capabilities

* feat: add declaration and definition support

* feat: add DocumentLink, DocumentColor

* feat: update LSP edits, replace SourceGraph code to deprecate sourceLength

* refactor: use an array of lines to store content, instead of storing the string, to make updates less expensive

* fix: update snippets for new syntax

* docs: add updated GIF

* fix: DidChange storage of updates
@a-h a-h closed this as completed Jun 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants