Skip to content

Latest commit

 

History

History
166 lines (122 loc) · 5.75 KB

File metadata and controls

166 lines (122 loc) · 5.75 KB

Plugin trust and cache behavior

Petra plugins are small by design. A plugin can add template functions with Funcs() and can parse helper templates with Apply().

That gives a plugin enough room to be useful, but it also means plugin helpers can bypass html/template escaping when they return trusted template types. This page describes the contract Petra currently follows.

Parse order

For each new template set, Petra does this:

  1. Collect Components(namespace, set) mounts and the render plugins required by those sets.
  2. Copy Template.FuncMap into a new function map.
  3. Add functions from required component-set plugins.
  4. Add functions from non-component plugins in the order listed in Template.Plugins.
  5. Attach the combined function map to the Go template.
  6. Apply required component-set plugins.
  7. Apply non-component plugins in Template.Plugins order.
  8. Compile mounted component sets and their private imports.
  9. Parse the page, layout, and app component templates.

ParseDir, ParseFS, and successful reloads all build a new template set. They do not mutate the active parsed templates in place.

Function name collisions

Component-set requirements override same-named functions from Template.FuncMap.

Explicit plugin functions override required component-set functions. If two explicit plugins return the same function name, the later plugin in Template.Plugins wins.

Keep plugin order explicit when names overlap.

The built-in plugins use these names:

  • HTML() adds attrs, html, and js.
  • Markdown() adds MarkdownToHTML and the callable {{Markdown}} helper.
  • SVG() adds the callable {{SVG}} helper.
  • Components(namespace, set) mounts exported templates from a ComponentSet.

The _loadMarkdown and _loadSVG functions are implementation details used by the helper templates. Do not call them from application templates.

Component sets

NewComponentSet(id, files, root, opts...) describes a reusable component library. Components(namespace, set) mounts that library into an app.

Definitions inside a component set are namespace-free:

{{define "TextField name label id attrs error?"}}
  <label for="{{.id}}">{{.label}}</label>
  <input id="{{.id}}" name="{{.name}}"{{range $k, $v := .attrs}} {{attrs $k $v}}{{end}}>
  {{if .error}}<span class="error">{{.error}}</span>{{end}}
{{end}}

The app chooses the public namespace:

ui := petra.NewComponentSet(
	"github.com/acme/petra-ui",
	uiFS,
	"components",
	petra.Requires(petra.HTML()),
)

tmpl := petra.NewWithOptions(petra.Options{
	Plugins: petra.Plugins{
		petra.Components("UI", ui),
	},
})

Application templates call exported definitions through the mounted namespace:

{{UI.TextField "email" "Email" "email" (dict "type" "email") .Errors.Email}}

Export uses Go's casing rule. TextField is public. fieldLabel and _fieldLabel are private.

A set can privately import another set:

base := petra.NewComponentSet("github.com/acme/petra-base", baseFS, "components")
kit := petra.NewComponentSet(
	"github.com/acme/petra-kit",
	kitFS,
	"components",
	petra.Import("Base", base),
)

Templates in kit can call {{Base.Button "Save"}} if Button is exported by base. The app does not get a Base namespace unless it mounts base too.

Use a live filesystem such as os.DirFS while developing a library. Add both the application template folder and the library folder to HotReloadOptions. Changes outside the application template root trigger a full template reparse, so external component-set edits update cleanly even though Petra's selective dependency graph remains app-template-root based.

Requires is for server-side render plugins only. Component sets do not manage client assets, JavaScript imports, CSS files, or model types in v1.

See component set architecture for the parse pipeline, privacy rules, and package shape.

Cache lifetime

The built-in Markdown and SVG plugins cache rendered output inside closures created by Funcs().

That means the cache belongs to one parsed template set. When Petra reparses templates, it calls Funcs() again and the new template set gets a fresh cache. Requests that are already using the old template set can keep using the old cache until those requests finish.

Missing Markdown and SVG files fail closed. Petra returns an execution error and does not emit partial trusted HTML for that helper call.

MarkdownToHTML renders the string it receives each time. It does not use the file cache.

Trusted helpers

These helpers are for repository-controlled content or HTML generated by your application:

  • html returns template.HTML.
  • js returns template.JS.
  • Markdown and MarkdownToHTML return template.HTML.
  • SVG returns template.HTML.

Do not pass user-authored content to those helpers as a sanitizer. They mark the result as trusted for html/template.

The SVG class argument is also trusted input. Petra replaces an existing class attribute in the raw SVG text; it is meant for app-owned class strings such as Tailwind classes.

attrs is different. It validates the attribute name, escapes the value, blocks on* and style attributes, and rejects unsafe URL schemes for URL attributes. It is still best used with app-chosen attribute names.

Custom plugins

A custom plugin should keep its trust boundary obvious:

  • Return plain strings when html/template should escape the result.
  • Return template.HTML, template.JS, or template.HTMLAttr only after the plugin has produced safe output itself.
  • Keep caches inside Funcs() when they should reset on template reparse.
  • Avoid helper names used by Petra's built-ins unless overriding them is intentional.

The package examples include a small custom plugin.