Make Writing HTML in Go a Breeze π
GTML is a compiler which converts .html
files into composable .go
functions. Think of it like JSX for go.
Turn this:
<div _component="Greeting">
<h1>Hello, $prop("name")</h1>
</div>
Into this:
func Greeting(name string) string {
var builder strings.Builder
builder.WriteString(`<div _component="Greeting" _id="0"><h1>Hello, `)
builder.WriteString(name)
builder.WriteString(`!</h1>`)
return builder.String()
}
With go 1.22.3
or later, clone the repo and build the binary on your system.
git clone https://github.com/phillip-england/gtml;
cd gtml;
go build -o gtml main.go;
Then you'll be left with a binary you can move onto your PATH.
mv gtml ./some/dir/on/your/path
_____ _______ __ __ _
/ ____|__ __| \/ | |
| | __ | | | \ / | |
| | |_ | | | | |\/| | |
| |__| | | | | | | | |____
\_____| |_| |_| |_|______|
---------------------------------------
Make Writing HTML in Go a Breeze π
Version 0.1.10 (2024-12-6)
https://github.com/phillip-england/gtml
---------------------------------------
Usage:
gtml [OPTIONS]... [INPUT DIR] [OUTPUT FILE] [PACKAGE NAME]
Example:
gtml --watch build ./components output.go output
Options:
--watch rebuild when source files are modified
In gtml, we make use of html attributes to determine a components structure. Here is a quick list of the available attributes:
- _component
- _for
- _if
- _else
- _slot
- _md
When gtml is scanning .html
files, it is searching for _component
elements. When it finds a _component
, it will generate a function in go which will output the _component
's html.
π¨
_component
may not be defined within another_component
. However, you can use a_component
as aplaceholder
within another_component
When defining a _component
, you must give it a name:
<button _component="CustomButton">Click Me!</button>
The above _component
will be converted into:
func CustomButton() string {
var builder strings.Builder
builder.WriteString(`<button _component="CustomButton" _id="0">Click Me!</button>`)
return builder.String()
}
_for
elements are used to iterate over a slice. The slice may be a custom type or a string slice.
_for
elements require their attribute value to be structured in the following way:
_for="ITEM OF ITEMS []TYPE"
Such as:
<div _component="ColorList">
<ul _for='color of colors []string'>
<p>$val(color)</p>
</ul>
</div>
We can also do the same with a slice of custom types:
<div _component="GuestList">
<ul _for='guest of Guests []Guest'>
<p>$val(guest.Name)</p>
</ul>
</div>
π¨ bring your own types, gtml will not autogenerate them (yet..? π¦)
_if
elements are used to render a piece of html if a condition is met.
input:
<div _component="AdminPage">
<div _if="isLoggedIn">
<p>you are logged in!</p>
</div>
</div>
_else
elements are used to render a piece of html if a condition is not met.
input:
<div _component="AdminPage">
<div _else="isLoggedIn">
<p>you are not logged in!</p>
</div>
</div>
_slot
elements are unique in the sense that they are not used within a _component
itself, rather, they are used in it's placeholder
.
We will discuss placeholders more in a bit, but for now, just know that a placeholder is what we refer to a _component
as when it is being used within another component.
For example, this LoginForm
uses CustomButton
as a placeholder
<form _component="LoginForm">
...
<CustomButton></CustomButton>
</form>
<div _component="CustomButton">
<button>Submit</button>
</div>
Now, imagine a scenario where you are going to use certain components on each page, like maybe each page of your website has the same navbar and footer?
That is where _slot
's are useful.
For example:
<div _component="GuestLayout">
<navbar>my navbar</navbar>
$slot("content")
<footer></footer>
<div>
<GuestLayout _component="HomePage">
<div _slot="content">
I will appear in the content section!
</div>
</GuestLayout>
_md
elements are used to render a markdown file into html. You can also provide a theme in _md-theme
. Here is a list of the available themes.
gtml uses goldmark under the hood to parse .md
files.
input:
<div _component="BlogPost">
<div _md="/path/to/file.md" _md-theme="dracula"></div>
</div>
In gtml, we make use of runes
to manage the way data flows throughout our components. Certain runes
accept string values while others expect raw values. Here is a quick list of the available runes in gtml:
- $prop()
- $val()
- $slot()
- $pipe()
$prop()
is used to define a prop
within our _component
. A prop
is a value which is usable by sibling and child elements. The value passed into $prop()
will end up in the arguments of our output function.
π¨:
$prop()
only accepts strings:$prop("someStr")
For example, we may define a $prop()
like so:
<div _component="RuneProp">
<p>Hello, $prop("name")!</p>
</div>
Once a $prop()
has been defined, it can used in elsewhere in the same component using $val()
. Also, you can pipe the value of a $prop()
into a child _component
using $pipe()
$val()
is used to access the value of prop
.
π¨:
$val()
only accepts raw values:$val(rawValue)
For example, here we make use of $val()
to access a neighboring prop
:
<div _component="Echo">
<p>$prop("message")</p>
<p>$val(message)</p>
</div>
$val()
is also used to access the data of iteration items in _for
elements.
For example:
<div _component="GuestList">
<ul _for='guest of Guests []Guest'>
<p>$val(guest.Name)</p>
</ul>
</div>
$pipe()
is used in situations where you want to pipe
data from a parent _component
into a child placeholder
.
π¨:
$pipe()
only accepts raw values:$pipe(rawValue)
For example:
<div _component="RunePipe">
<p>Sally is $prop("age") years old</p>
<Greeting age="$pipe(age)"></Greeting> <== piping in the age
</div>
<div _component="Greeting">
<h1>This age was piped in!</h1>
<p>$prop("age")</p>
</div>
When a _component
is used within another _component
, we refer to it as a placeholder
. placeholders
enable us to mix and match components with ease.
π¨: This is not JSX and you cannot do self-closing tags like
<SomeComponent/>
. All tags must consist of both an opening tag and closing tag. This can be supported in future versions.
For example, this LoginForm
uses CustomButton
as a placeholder
<form _component="LoginForm">
...
<CustomButton></CustomButton>
</form>
<div _component="CustomButton">
<button>Submit</button>
</div>
You may pass data into a placeholder
using it's attributes. These attributes must corrospond to the target _component
's props
.
π¨: when passing values into
placeholder
attributes, you must refer to the attributes in kebab-casing. This will be patched in future version. For example, below we do$prop("firstName")
to definefirstName
, but when we pass values into the_element
we usefirst-name
instead.
For example:
<div _component="NameTag">
<h1>$prop("firstName")</h1>
<p>$prop("message")</p>
</div>
<NameTag _component="PlaceholderWithAttrs" message="is the best" first-name="gtml"></NameTag>
If a placeholder
needs to access a value from a parent _component
, the value may be piped in using the $pipe()
rune
.
For example: For example:
<div _component="RunePipe">
<p>Sally is $prop("age") years old</p>
<Greeting age="$pipe(age)"></Greeting> <== piping in the age
</div>
<div _component="Greeting">
<h1>This age was piped in!</h1>
<p>$prop("age")</p>
</div>
This section contains notes related to the ongoing development of gtml.
- Solid Error Handling
- _component validations ran prior to building
- implement the $ctx() rune - stores a value in a global context which is made available to children and avoids the used of $pipe()
- implement the $var() rune - creates a local variable (meaning it cannot be used in $pipe())
- $md() rune support - enable the ability to inline markdown content into components
- _components cannot be named a traditional html tag name β
- required _components to have a name β
- _components cannot have the same name β
- JSX support (preprocessing required)
- camelCase Supported in Attributes (preprocessing required)
- Type Generation (feels more like a luxery feature?)
- Output Cleanup (again, luxery?)
- allow the command line tool to take in a single file instead of a dir as well (not vital)
- in this reddit convo, I talk to someone about growing out the buffers in the output components, need to do this!
- What if we place an invalid rune into one of our attributes?
- take into consideration which funcs are private / public
- how to catch and report free-floating runes?