Documentation
¶
Overview ¶
Package kyse compiles a view into Go.
The name is guarani for knife. The directives are the ones a template language is expected to have, with the names people already use for them, so a view is written on the first day without learning a syntax.
The file ¶
The source is `home.kyse.go` and the output is `home.go`, side by side in the same package. The extension ends in `.go` because the host language is part of the name -- which is what lets the compiler see the file and skip it.
//go:build kyse
package views
@go
type HomeData struct{ Name string }
@endgo
@extends('layouts.app')
@section('content')
<h1>Olá {{ .Name }}</h1>
@endsection
The build tag is what makes it legal. Go reads the constraint, sees the file is excluded, and stops at the package clause -- it never parses the markup below. Verified on Go 1.26.5: build, vet, test and run all pass with the generated file in the same directory. `gofmt` is the only tool that ignores build constraints, and the CI calls it filtering `*.kyse.go`.
What it is not ¶
It is not a template engine with a runtime. There is no parse at boot, no cache directory, no reflection over the data. The output is a Go function that writes strings, and the data is the struct the view declared in its `@go` block -- so a field that does not exist is a compile error rather than a blank page. That is the whole reason to have written this instead of using html/template.
Index ¶
- Constants
- func Directives() []string
- func Generate(f *File, name, dataType, output string) ([]byte, error)
- func Name(viewsDir, source string) string
- func OutputPath(source string) string
- func PageType(f *File) string
- func RenderType(f *File) string
- type Block
- type Error
- type Errors
- type File
- type Kind
- type Node
- type Section
Constants ¶
const CompiledDir = "storage/framework/views"
CompiledDir is compiledPath as a caller can use it, without the trailing separator.
Exported so that whatever sweeps the compiled tree and whatever writes into it read the same constant. Two string literals for one directory is how a sweeper comes to look somewhere the writer never wrote.
Variables ¶
This section is empty.
Functions ¶
func Directives ¶ added in v0.11.0
func Directives() []string
Directives returns every directive kyse knows, block and inline, sorted.
It exists so a test can walk the whole set. Three times now a directive was declared here and had no case in the generator, and each time the node was dropped in silence: @else made both halves of an if appear at once, and @for and @while emitted the loop with an empty body. The build stays green, the command reports success, and the page is simply missing what the author wrote.
A closed set is only a promise if something checks that every member of it does something.
func Generate ¶
Generate turns a parsed view into the Go that renders it.
The output is a function that writes strings to an io.Writer, plus an init() that registers it under the view's name. No runtime parse, no cache directory, no reflection: the data is the struct the view declared in its `@go` block, and a field that does not exist stops the build.
A template engine usually compiles to source at run time and caches the result. Same idea, moved to build time -- which is what makes the typo a compile error instead of a warning nobody reads in production.
Why the output path is a parameter ¶
The generated Go carries `//line` directives, so the compiler reports a type error inside `{{ }}` at the line of the `.kyse.go` the person wrote. A directive stays in effect until the next one, which means the scaffolding between two interpolations has to be handed back to the generated file by name -- and this function is the only place that knows which lines are the view's and which are its own. Guessing the name from f.Path is not possible: The output is `auth/login.go` beside `auth/login.kyse.go`, and deriving one name from the other here would put the rule in two places -- OutputPath is where it lives, and a caller that disagreed with it would write the directive for a file that is not the one on disk.
Both paths are written into the output verbatim -- the Go compiler prints a //line file name exactly as it finds it, without resolving it -- so passing them relative to the project root is what makes the reported position clickable from where `go build` runs.
func Name ¶
Name is the name a view is rendered by: the path under resources/views, with dots. `auth/login.kyse.go` is rendered as "auth.login".
func OutputPath ¶
OutputPath says where the Go generated from a view goes: under storage, mirroring the tree of the source.
resources/views/auth/login.kyse.go -> storage/framework/views/auth/login.go
It is build output, so it is gitignored: written beside the source, every `aru view:build` would put 28 files nobody wrote into `git status`, and a reviewer would have to skip them in every review.
Each directory of views is its own Go package, named by the person in the source's package clause and copied verbatim into the output. That is the ordinary arrangement of a Go tree, and it is what keeps `resources/views/` readable: opening `auth/` shows the four screens of `auth/` and nothing else.
The output does not land flat. Go has one package per directory, which is what makes flattening look necessary -- but a nested package imports what it needs like any other, and the chrome a layout declares lives in `framework/view`, which every generated file already imports.
The cost is one blank import per directory of views in bootstrap, next to the one that was already there. That is the registration the whole design is built on -- the same shape as a database/sql driver -- and paying it per directory rather than once is what fifteen files leaving the top level costs.
func PageType ¶ added in v0.10.0
PageType is the type a view declared, and what a layout publishes to the views that extend it.
A declaration or an alias, and the alias is the interesting one:
@go type LoginData = authhttp.LoginData @endgo
It is how a screen renders a struct the controller owns. The struct belongs with the code that fills it -- that is where the fields are set and where a missing one is a compile error -- and repeating it in the view would be two declarations of one shape, kept in step by hand.
The view still says which type it draws, in one line, and that line is what makes `{{ .Email }}` compile or not.
func RenderType ¶ added in v0.10.0
RenderType is the type a view asserts the data to before drawing.
For a layout that is the interface, when it declares one. That is the whole fix for a defect that cost a full cycle: the starter kit installs a layout whose @go block declares the sign-in struct, and reading the first type for both questions made the layout render with THAT struct. From then on every page `aru make:module` generated answered
view "layouts.app" takes AuthPage and got views.InvoicesIndexData
with `go build` green, because a type assertion fails at run time and the routes hide it: the wiring is manual, so the page 404s before anyone reaches the 500.
For a page it is the struct, because a page interpolates fields.
A layout that declares no interface renders with view.Layout, the contract framework/view publishes and that view.Page satisfies. That is the ordinary case now: the chrome every application drew identically -- the title, the brand, the token, the four navigation links -- moved into the framework, so a layout only declares an interface when it wants a different one.
Types ¶
type Block ¶ added in v0.14.0
type Block struct {
// Body is the Go between the two directives, unchanged.
Body string
// Line is the 1-indexed source line the body starts on -- the line after
// `@go`, not the `@go` itself.
Line int
}
Block is one `@go … @endgo` body, with the line its first line came from.
The line is what lets a type error inside the block name the view instead of the generated file. The body is copied verbatim, so the Nth line of Body is the (Line+N-1)th line of the source, and one directive at the top is enough to carry the whole block.
type Error ¶
type Error struct {
Path string
Line int
Message string
// Hint is what to do about it, when there is something to say.
Hint string
}
Error is a compile error that names the source position.
The whole point of writing a compiler instead of using a template library is that this can be exact. `resources/views/home.kyse.go:12: @endsection with no @section` is a sentence somebody acts on; a stack trace into a generated file is not.
type Errors ¶
type Errors []*Error
Errors is more than one problem, reported together.
At once, not at the first: a person fixing a view should not discover the problems one build at a time. Same reasoning as the spec validator.
func (Errors) Unwrap ¶ added in v0.31.0
Unwrap exposes the problems one at a time, so errors.As reaches a position through the group.
Without it a caller asking whether a failure names a line got no for an answer whenever there was more than one problem, which is the case where the answer matters most.
type File ¶
type File struct {
// Package is the clause the Go compiler stops at.
Package string
// Imports are the lines of the import block, when the view has one, copied
// verbatim into the generated file. It is how a view draws a component that
// lives in another package.
Imports []string
// Go is the content of the @go blocks, in order, copied verbatim.
Go []Block
// Extends is the layout this view extends, or empty.
Extends string
// Sections are the named blocks, in declaration order.
Sections []Section
// Body is the top-level content of a view that extends nothing -- a layout
// is written this way.
Body []Node
// Path is the source file, for error messages.
Path string
}
File is a parsed view.
func Parse ¶
Parse reads a view and returns its tree.
It reports every problem it can find rather than stopping at the first, for the same reason the spec validator does: somebody fixing a view should not discover the mistakes one build at a time.
func (*File) IsLayout ¶ added in v0.10.0
IsLayout reports whether this file is a layout.
The distinction is not a naming convention: a layout receives the sections a child declared, and a page does not. The source says which by containing the directive that reads them.
It matters for the data type too. A layout renders with the interface it declares, so any page that satisfies it can be drawn inside; a page renders with its own struct, because it interpolates fields. Reading one type for both is what made the starter kit install a layout typed by the sign-in struct, and from then on every page `aru make:module` generated failed to render -- with a green `go build`, because the disagreement is a type assertion at run time.
type Kind ¶
type Kind int
Kind is what a piece of a view is.
const ( // Text is literal markup, written out as-is. Text Kind = iota // Echo is `{{ expr }}` -- escaped. Echo // Raw is `{!! expr !!}` -- not escaped. Raw // Directive is `@name(args)`. Directive // GoBlock is the body between `@go` and `@endgo`, copied verbatim into the // generated file. GoBlock )
type Node ¶
type Node struct {
Kind Kind
// Name is the directive name for Directive, empty otherwise.
Name string
// Body is the text, the expression, or the directive arguments.
Body string
// Children are the nodes inside a block directive.
Children []Node
// Line is the 1-indexed line in the source file.
Line int
}
Node is one piece of a parsed view, with the position it came from.
The position is the reason this exists as a tree rather than a string rewrite: an error has to name the line of the `.kyse.go`, not of the generated file. An engine that compiles at run time can only guess at this -- recompile with markers, search, give up after twenty lines -- and we do not have to guess, because we emit the Go ourselves.