giom

package module
v0.0.0-...-9c1be47 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 20 Imported by: 0

README

Giom

Giom is an indentation-based template language for Go applications. It compiles GION templates to Gad bytecode and is designed for server-side rendering with a small API, reusable components, slots, imports, and HTML-oriented syntax.

The current project root contains the new implementation. The old implementation and old samples were removed.

Features

  • Indentation-based HTML templates
  • Components with named parameters and named slots
  • @import friendly template organization
  • Gad expressions and statements inside templates
  • HTML tag shorthand for ids, classes, and attributes
  • Transpilation to Gad AST/source for inspection
  • Go embedding through Compile and Gad VM execution
  • CMS example application in examples/cms

Quick Template

@main
    !!! 5
    html[lang="en"]
        head
            title Hello
        body
            main.container
                h1 {= Title}
                p Welcome to Giom.

Component Example

@export comp page(title)
    !!! 5
    html
        head
            title {= title}
        body
            @slot main

@main
    +page("Docs")
        h1 Documentation
        p This content is passed to the main slot.

Go Usage

package main

import (
    "bytes"
    "log"

    "github.com/gad-lang/gad"
    "github.com/gad-lang/giom"
)

func main() {
    src := []byte(`@main
    p Hello {= Name}
`)

    builtins := giom.AppendBuiltins(gad.NewBuiltins())
    st := gad.NewSymbolTable(builtins.NameSet)
    if _, err := st.DefineGlobals([]string{"Name"}); err != nil {
        log.Fatal(err)
    }

    _, bc, err := giom.Compile(st, src, gad.CompileOptions{})
    if err != nil {
        log.Fatal(err)
    }

    var out bytes.Buffer
    vm := gad.NewVM(builtins.Build(), bc)
    if _, err := vm.RunOpts(&gad.RunOpts{
        StdOut:  &out,
        Globals: gad.Dict{"Name": gad.Str("Giom")},
    }); err != nil {
        log.Fatal(err)
    }

    log.Print(out.String())
}

Documentation

Repository Layout

.
├── compiler.go          # Giom compiler entry points
├── builtins.go          # HTML and write builtins
├── render.go            # High-level Render struct with caching
├── importer.go          # FileImporter for @import resolution
├── node/                # Giom AST nodes and Gad conversion
├── parser/              # Indentation parser and scanner
├── token/               # Giom token definitions
├── examples/cms/        # Full CMS example
└── docs/                # User documentation

CMS Example

cd examples/cms
go run .

Open http://localhost:8080/. The app creates cms.db on first run and seeds the database from seed.yaml only when cms.db does not already exist.

Benchmarks (CMS Example)

Results from examples/cms on AMD Ryzen 7 5700G.

Cold vs Warm Render Time
Cold vs Warm render time chart

Bytecode caching reduces render time by 2–3× across all template types. Cold (first) render includes compilation; warm (subsequent) renders reuse cached bytecode. Run with go test -bench=BenchmarkColdVsWarmChart ./examples/cms.

Warm (Cached) Template Routes (500ms benchtime, 3 runs)
Route ns/op B/op allocs/op
/ (index) 1,027,000 749,424 7,908
/pages/about 678,287 574,279 4,438
/pages/guides 666,918 567,403 4,271
/pages/contact 677,064 574,323 4,438
/posts/designing-fast-editorial-pages 819,535 629,675 5,488
/posts/sqlite-compact-cms 792,234 615,586 5,157
/posts/modern-admin-interfaces 806,615 617,288 5,190
/posts/reusable-gion-components 809,749 621,317 5,288
/posts/shipping-friendly-first-page 795,117 619,889 5,257
/posts/building-gallery-component 827,598 633,974 5,588
/tags/design 1,053,072 706,459 6,943
/tags/engineering 1,038,128 700,075 6,804
/tags/news 979,927 664,407 6,021
/tags/tutorials 966,278 670,582 6,154
Multi-Request Benchmarks
Benchmark ns/op B/op allocs/op
SequentialNavigation (14 pages) 12,001,483 9,080,927 79,790
ColdRender (compile + render) 25,082,682 2,338,816 28,114
MixedWorkload (4 pages) 3,580,932 2,701,861 25,022
PostWithRelatedContent (4 posts) 3,232,344 2,515,971 21,211
TagWithPagination (4 tags, page=1) 4,052,386 2,777,445 26,019

ColdRender creates a fresh server for each iteration (no cache). All others share a single server (bytecode cached after first request). Show with go test -bench=. -benchmem ./examples/cms.

Build

go build ./...

For the CMS module:

cd examples/cms
go build .

License

See LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	BuiltinEscape = &gad.Function{
		FuncName: "giom$escape",
		Value: func(call gad.Call) (_ gad.Object, err error) {
			if err = call.Args.CheckLen(1); err != nil {
				return
			}

			var (
				value = call.Args.GetOnly(0)
				s     string
			)

			switch t := value.(type) {
			case gad.RawStr:
				s = string(t)
			case gad.Str:
				s = string(t)
			default:
				var v gad.Object
				if v, err = call.VM.Builtins.Call(gad.BuiltinStr, call); err != nil {
					return
				}
				s = string(v.(gad.Str))
			}

			return gad.RawStr(s), nil
		},
	}

	AttrFunc = func(vm *gad.VM, name, value gad.Object) (ret gad.RawStr, err error) {
		var (
			toRawStr = vm.Builtins.ArgsInvoker(gad.BuiltinRawStr, gad.Call{VM: vm})
		)

		if value.IsFalsy() {
			return
		}

		if _, ok := name.(gad.RawStr); !ok {
			if name, err = toRawStr(name); err != nil {
				return
			}
		}

		switch t := value.(type) {
		case gad.Array:
			var b strings.Builder
			for _, o := range t {
				if o.IsFalsy() {
					continue
				}
				if _, ok := o.(gad.RawStr); !ok {
					if o, err = toRawStr(o); err != nil {
						return
					}
				}
				b.WriteString(string(o.(gad.RawStr)))
				b.WriteString(" ")
			}
			value = gad.RawStr(strings.TrimSpace(b.String()))
		case gad.RawStr:
		case gad.Flag:
			if t {
				return gad.RawStr(name.ToString()), nil
			}
			return "", nil
		default:
			if value, err = toRawStr(value); err != nil {
				return
			}
		}
		return gad.RawStr(name.ToString() + "=" + strconv.Quote(value.ToString())), nil
	}

	BuiltinAttr = &gad.Function{
		FuncName: "giom$attr",
		Value: func(call gad.Call) (ret gad.Object, err error) {
			if err = call.Args.CheckLen(2); err != nil {
				return
			}

			ret, err = AttrFunc(call.VM, call.Args.GetOnly(0), call.Args.GetOnly(1))
			return
		},
	}

	BuiltinAttrs = &gad.Function{
		FuncName: "giom$attrs",
		Value: func(call gad.Call) (_ gad.Object, err error) {
			var (
				b      strings.Builder
				rs     gad.RawStr
				keys   []string
				d      = make(gad.Dict)
				class  []string
				style  []string
				filter func(arr gad.Array) (ret gad.Array)
			)

			filter = func(arr gad.Array) (ret gad.Array) {
				for _, v := range arr {
					switch t := v.(type) {
					case *gad.KeyValue:
						if !t.K.IsFalsy() {
							ret = append(ret, t.V)
						}
					case gad.Array:
						ret = append(ret, filter(t)...)
					default:
						if !t.IsFalsy() {
							ret = append(ret, t)
						}
					}
				}
				return
			}

			cb := func(na *gad.KeyValue) (err error) {
				var k string
				switch t := na.K.(type) {
				case gad.Bool:

				case gad.Str:
					k = string(t)
				case gad.RawStr:
					k = string(t)
				default:
					var s gad.Str
					if s, err = gad.ToStr(call.VM, t); err != nil {
						return
					}
					k = string(s)
				}
				switch k {
				case "class":
					if !na.V.IsFalsy() {
						switch t := na.V.(type) {
						case gad.Str:
							if len(t) > 0 {
								class = append(class, string(t))
							}
						case gad.RawStr:
							if len(t) > 0 {
								class = append(class, string(t))
							}
						case *gad.KeyValue:
							if !t.K.IsFalsy() {
								var arr gad.Array
								switch t := t.V.(type) {
								case gad.Array:
									arr = t
								default:
									arr = gad.Array{t}
								}
								for _, o := range filter(arr) {
									var s gad.Str
									if s, err = gad.ToStr(call.VM, o); err != nil {
										return
									}
									class = append(class, string(s))
								}
							}
						case gad.Array:
							for _, o := range filter(t) {
								var s gad.Str
								if s, err = gad.ToStr(call.VM, o); err != nil {
									return
								}
								class = append(class, string(s))
							}
						}
					}
				case "style":
					switch t := na.V.(type) {
					case gad.Str:
						if len(t) > 0 {
							style = append(style, string(t))
						}
					case gad.RawStr:
						if len(t) > 0 {
							style = append(style, string(t))
						}
					case *gad.KeyValue:
						if !t.K.IsFalsy() {
							var arr gad.Array
							switch t := t.V.(type) {
							case gad.Array:
								arr = t
							default:
								arr = gad.Array{t}
							}
							for _, o := range filter(arr) {
								var s gad.Str
								if s, err = gad.ToStr(call.VM, o); err != nil {
									return
								}
								style = append(style, string(s))
							}
						}
					case gad.Array:
						for _, o := range filter(t) {
							var s gad.Str
							if s, err = gad.ToStr(call.VM, o); err != nil {
								return
							}
							style = append(style, string(s))
						}
					case gad.Dict:
						for key, o := range t {
							var s gad.Str
							if s, err = gad.ToStr(call.VM, o); err != nil {
								return
							}
							style = append(style, key+":"+string(s))
						}
					}
				default:
					v := na.V
					switch t := v.(type) {
					case *gad.KeyValue:
						if t.K.IsFalsy() {
							return
						}
						v = t.V
					}
					if _, ok := d[k]; !ok {
						keys = append(keys, k)
					}
					d[k] = v
				}
				return err
			}

			err = call.NamedArgs.Walk(func(na *gad.KeyValue) (err error) {
				var v gad.Object = na
				switch t := na.K.(type) {
				case gad.Bool:
					if !t {
						return
					}
					v = na.V
				}
				switch t := v.(type) {
				case gad.Array:
					return gad.ItemsOfCb(call.VM, &gad.NamedArgs{}, cb, na.V)
				case *gad.KeyValue:
					return cb(t)
				case gad.KeyValueArray:
					for _, value := range t {
						if err = cb(value); err != nil {
							return
						}
					}
					return
				default:
					return cb(na)
				}
			})

			if err != nil {
				return
			}

			for _, key := range keys {
				if rs, err = AttrFunc(call.VM, gad.Str(key), d[key]); err == nil && rs != "" {
					b.WriteString(" " + string(rs))
				}
			}

			if len(class) > 0 {
				b.WriteString(" class=")
				b.WriteString(strconv.Quote(strings.Join(class, " ")))
			}

			if len(style) > 0 {
				b.WriteString(" style=")
				b.WriteString(strconv.Quote(strings.Join(style, "; ")))
			}

			return gad.RawStr(b.String()), nil
		},
	}

	BuiltinTextWrite = &gad.Function{
		FuncName: "giom$write",
		Value: func(call gad.Call) (_ gad.Object, err error) {
			return call.VM.Builtins.Call(gad.BuiltinWrite, call)
		},
	}
)

Functions

func AppendBuiltins

func AppendBuiltins(b *gad.Builtins) *gad.Builtins

AppendBuiltins registers giom built-in functions (escape, attr, attrs, write) into the given Builtins and returns it.

func Compile

func Compile(st *gad.SymbolTable, input []byte, opts gad.CompileOptions) (*giomnode.File, *gad.Bytecode, error)

Compile parses giom v2 source and compiles it to GAD bytecode.

func CompileFallback

func CompileFallback(c *gad.Compiler, nd ast.Node) error

CompileFallback compiles Giom-specific AST nodes through a Gad compiler.

func CompileFile

func CompileFile(st *gad.SymbolTable, module *gad.ModuleSpec, file *giomnode.File, opts gad.CompileOptions) (*gad.Bytecode, error)

CompileFile compiles a parsed giom v2 file to GAD bytecode.

func Transpile

func Transpile(name string, src []byte, outPath string) error

Transpile parses Giom source and writes the converted Gad source to outPath.

Types

type FileImporter

type FileImporter struct {
	NameResolver  func(cwd, name string) (string, error)
	WorkDir       string
	FileReader    func(string) (data []byte, uri string, err error)
	TranspilePath func(srcPath string) string
	// contains filtered or unexported fields
}

FileImporter imports Gad and Giom files from the filesystem.

Files ending in .giom are returned as gad.BuiltinCompileModuleFunc so they are parsed and compiled with Giom syntax during Gad import compilation.

func (*FileImporter) Fork

func (m *FileImporter) Fork(moduleName string) gad.ExtImporter

Fork returns an importer rooted at the imported module's directory.

func (*FileImporter) Get

func (m *FileImporter) Get(name string) gad.ExtImporter

Get returns this importer for a non-empty module name.

func (*FileImporter) Import

func (m *FileImporter) Import(ctx context.Context, module *gad.ModuleSpec) (data any, uri string, err error)

Import reads the resolved module. Giom modules are compiled through a builtin module compiler function; other files are returned as source bytes.

func (*FileImporter) Name

func (m *FileImporter) Name() (string, error)

Name resolves the current import name into the compiler module cache key.

type Render

type Render struct {
	// TemplateDelay is the debounce duration before recompiling after
	// a file change is detected. Defaults to 15s.
	TemplateDelay time.Duration

	// TranspilePath returns the output path for transpiled .gad files.
	// If nil, transpilation is skipped.
	TranspilePath func(srcPath string) string

	// BuiltinsFunc returns the Gad builtins to use for compilation.
	// If nil, defaults to AppendBuiltins(gad.NewBuiltins()).
	BuiltinsFunc func() *gad.Builtins

	ModuleMapFunc func(mm *gad.ModuleMap) *gad.ModuleMap
	// contains filtered or unexported fields
}

Render handles Giom template rendering with bytecode caching and automatic recompilation on file changes. It is safe for concurrent use.

func NewRender

func NewRender(workDir string) *Render

NewRender creates a Render with the given workDir. Non-empty paths are resolved to an absolute path. Other fields (TemplateDelay, TranspilePath, BuiltinsFunc) may be set on the returned value before use.

func (*Render) OnRender

func (r *Render) OnRender(f ...func(first bool, mainFile string, files []string, lastTime time.Time, err error)) *Render

OnRender appends callback functions invoked after compilation. On first render, first=true with empty files and zero lastTime. On recompilation due to file changes, first=false with the changed file paths. If compilation fails, err is non-nil and the cached entry is not updated. Returns the Render for chaining.

func (*Render) Render

func (r *Render) Render(out io.Writer, filePath string, globals gad.Dict) error

Render reads the Giom template at filePath, compiles or retrieves cached bytecode, and executes it with the keys of globals available as global variables, writing the output to out.

func (*Render) WorkDir

func (r *Render) WorkDir() string

WorkDir returns the base directory for resolving module imports.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL