miniskin

package module
v0.3.10 Latest Latest
Warning

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

Go to latest
Published: May 8, 2026 License: MIT Imports: 16 Imported by: 0

README

miniskin

Go Reference

  • Miniskin is a build-time template assembler for Go projects.
  • Supports Mockup-Driven Development (MDD): mockup HTML files can serve directly as source files — they render standalone in the browser and are processed at build time, eliminating the need to maintain separate mockup and source files.
  • It processes content files using an explicit asset catalog defined in *.miniskin.xml files and generates Go source code with //go:embed directives.
  • It is designed to run during development as part of go generate, not at runtime.
  • The tool is content-agnostic: files are treated as opaque text except for optional front-matter and percent-tag directives.
  • The parser is implemented as a finite-state machine, ensuring deterministic single-pass processing.
  • Percent-tag syntax (<% ... %>) coexists with Go template syntax ({{ ... }}) without conflicts.

Security notice

miniskin performs no input sanitization, no escaping of user-supplied data, and no protection against code injection. It is a build-time tool. Do not expose it to untrusted input.

Install

CLI (recommended — primary entry point):

go install github.com/ot4go/miniskin/cmd/miniskin@latest

Library (for use with go:generate or programmatic invocation):

go get github.com/ot4go/miniskin

Why miniskin?

Embedding assets in Go projects typically involves scanning directories with //go:embed patterns or scattering embed directives across packages. Registering those assets in an HTTP mux requires manual wiring that drifts as assets are added or removed. Layouts (headers, footers, navigation) get duplicated across templates and diverge over time.

miniskin addresses these problems with an explicit XML catalog that declares exactly which files are embedded, reusable skin (layout) files that enforce structural consistency, and a code generator that produces the embed declarations and asset registration functions automatically.

Design goals

  1. Explicit asset catalog — every embedded file is declared in XML; nothing is implicitly scanned
  2. Minimal embedded payload — only declared assets are included in the binary
  3. Build-time integration — runs via go generate, produces deterministic output
  4. No syntax conflict — percent-tag syntax (<% %>) passes Go {{ }} templates through untouched
  5. Content-agnostic processing — files are opaque text; percent tags and front-matter are the only interpreted structures
  6. Layout and content separation — skins provide reusable layouts; content files declare which skin to apply via front-matter

Concepts

Percent tags

Six equivalent syntaxes, resolved at generation time:

Syntax Behavior
<%var%> value, escaped per <escape> rules
<%%var%%> value, never escaped
<!--%var%--> same as <%>, hidden as an HTML comment
<!--%%var%%--> same as <%%>, hidden as an HTML comment
/*<%var%>*/ same as <%>, hidden as a JS / CSS comment
/*<%%var%%>*/ same as <%%>, hidden as a JS / CSS comment

Double percent tags also support includes: <%%include:/path/to/file%%>

The JS-comment wrapper (/*<%, %>*/) keeps tags valid inside .js / .css files so they read as block comments when loaded raw (useful during mockup development). Apertura and closure are independent — a tag opened with /*<% may close with %> (the */ is not consumed) and vice versa.

Escape types

All tags default to no escaping. Use <escape> in XML to configure default escaping per file extension, or use an explicit escape prefix in any tag syntax:

<%url:var%>          URL-encoded value
<%%js:var%%>         JS-escaped value
<!--%sql:var%-->     SQL-escaped (browser-invisible)
<!--%%json:var%%-->  JSON-escaped (browser-invisible)
Prefix Description Example input Example output
html HTML entities <b>"hi"</b> &lt;b&gt;&#34;hi&#34;&lt;/b&gt;
xml XML entities (&apos; for ') it's <ok> it&apos;s &lt;ok&gt;
url URL encoding hello world hello+world
js JavaScript string escaping say "hi"\n say \"hi\"\\n
css CSS hex escaping url("x") url\28 \22 x\22 \29
json JSON string escaping line1\nline2 line1\\nline2
sql SQL single-quote doubling O'Brien O''Brien
sqlt SQL LIKE (sql + _% escaping) 100% O'B_x 100\% O''B\_x

The escape:echo:text form applies an explicit escape to literal text: <%js:echo:it's "ok"%> outputs it\'s \"ok\".

Default escape rules

The <escape> element configures the default escape type based on file extension. It can appear in any XML block (<miniskin>, <bucket-list>, <bucket>, <resource-list>) and cascades to children:

<miniskin>
  <escape ext="*.html,*.html.tmpl" as="html" />
  <escape ext="*.js,*.js.tmpl" as="js" />
  <escape ext="*.css" as="css" />
  <escape ext="*.sql" as="sql" />
  <bucket-list filename="embed.go" module="content">
    <bucket src="app" dst="/gen.go" module-name="app">
      <escape ext="*.json" as="json" />
    </bucket>
  </bucket-list>
</miniskin>

When processing a source file, the default escape is determined by matching the file against <escape> rules. If no rule matches, no escaping is applied.

Individual items can override with escape="type":

<item type="static" src="data_src.txt" file="data.txt" escape="sql" />

Position of <escape> elements within a block is irrelevant. Child rules override parent rules for the same extension pattern.

Directives
Directive Description
if:var Include content if var is defined and non-empty
if-not:var Include content if var is undefined or empty
elseif:var Checked only if all previous branches were false
elseif-not:var Negated elseif
else Fallback branch
endif Close conditional block
end Universal closer (works for if, mockup-export, mockup-import)
end-if Close if block (specific alias)
end-mockup-export Close mockup-export block (specific alias)
end-mockup-import Close mockup-import block (specific alias)
note:text Discarded silently (comment)
echo:text Emit text (uses default escape)
include:path Include file contents (double tags only, resolved recursively)
include-notes:path Include only the bodies of note: tags from the file (double tags only). Used to assemble per-component documentation into a single Markdown
doc-block-begin:NAME / doc-block-end:NAME Capture content between the markers into the named buffer ms.docBuffer[NAME]; the captured region is not emitted in place
doc-block-content:NAME Emit the captured contents of the named buffer
doc-block-toc:NAME Emit a nested unordered list of the H1/H2 headers in the named buffer, with GitHub-compatible anchors
mockup-export:path [mode] Extract content to file (mockup mode only)
mockup-import:path [indent:N|Ntab] Insert file contents (mockup mode only)

All directives work in all four tag syntaxes. include: requires double percent tags (<%%include:path%%>). Examples:

<!--%%if:mockup%%-->
  <tr><td>Sample Data</td></tr>
<!--%%endif%%-->

<%note: this text is discarded%>

<!--%%echo:<script>alert("literal")</script>%%-->

<!--%%mockup-export: "/app/assets/css/login.css" append%%-->
.login { padding: 20px; }
<!--%%end%%-->
Front-matter

Files with a src attribute can have YAML-like front-matter delimited by ---:

---
skin: default
title: Sign In
css: /assets/signin.css
---
<div class="login">
  <h1>{{.AppName}}</h1>
</div>

Front-matter variables are available as percent-tag values. The skin key is special — it triggers skin application and is not passed as a variable.

Skins

A skin is an HTML layout file in the skin directory (default _skin/) that uses percent tags:

<!DOCTYPE html>
<html>
<head><title><%title%></title></head>
<body>
<%%content%%>
</body>
</html>

<%%content%%> is replaced with the processed body. Other front-matter variables (like <%title%>) are available in any tag syntax. Escaping is determined by the <escape> rules declared in the XML.

The skin directory cascades: <miniskin skin-dir><bucket skin-dir><resource-list skin-dir>. Default is _skin.

Conditionals
<%if:user%>
  <p>Welcome, <%user%></p>
<%elseif:guest%>
  <p>Guest access</p>
<%else%>
  <p>Please sign in</p>
<%endif%>

Negated variants with if-not: and elseif-not::

<!--%%if-not:production%%-->
  <div class="debug-bar">Debug Mode</div>
<!--%%endif%%-->

Blocks can be nested. Undefined variables inside a false branch do not cause errors.

Includes

Fragment files included via <%%include:/path%%>:

  • Resolved relative to contentPath
  • Can contain their own percent tags (resolved before insertion)
  • No front-matter, no skin — raw fragments only
  • Never written to disk — resolved in memory
  • Cycle detection: if A includes B includes A, generation fails
Doc-block buffers

doc-block-begin/end capture a region of resolved content into a labeled, in-memory buffer instead of emitting it where the markers stand. The buffer can later be replayed verbatim with doc-block-content or summarised with doc-block-toc:

<%% doc-block-begin: components %%>
<%% include-notes:btn_grid.js %%>
<%% include-notes:clock_display.js %%>
<%% include-notes:credential_password.js %%>
<%% doc-block-end: components %%>

# Contents

<%% doc-block-toc: components %%>

---

<%% doc-block-content: components %%>
  • Scope: bucket-global. A buffer captured in one resource-list item is visible from any other item in the same bucket. ms.docBuffer is reset at the start of each bucket.
  • Order independence: the build embed step runs each bucket twice — a dry pre-pass that only populates buffers (no output written, doc-block-toc/content emit nothing), then the regular pass with the buffers fully populated. Capture and emit can therefore live in different items in any order.
  • TOC format: doc-block-toc walks the captured markdown for # and ## lines, emitting a nested unordered list. Anchors are slugified GitHub-style (lowercase, alphanumerics and hyphens; duplicates get -1, -2, …). Lines inside fenced code blocks (``` or ~~~) are ignored.
  • Errors: referencing an unknown buffer with doc-block-toc or doc-block-content is an error during the regular pass.

Mockup processing

Mockup mode

Mockup files are HTML files designed to render standalone in a browser. They are declared in a <mockup-list> inside subdirectory *.miniskin.xml files (not at the root level). Mockup lists do not use a file attribute on items — only src.

<mockup-list save-mode="append">
    <var name="policybanner" value="1" />
    <item src="login_mockup.html">
        <var name="title" value="Sign In" />
    </item>
</mockup-list>

In mockup mode:

  • The variable mockup is automatically set to 1
  • Variables are not resolved<%title%> passes through literally
  • Conditionals check existence only — whether a variable is defined, not its value
  • mockup-export extracts content to files (see below)
  • The main output is discarded — only mockup-export side effects matter

Variable merge order: globals → mockup-list vars → item vars → front-matter vars. Skins are applied if declared in front-matter.

mockup-export / mockup-import

The mockup-export directive extracts content from a mockup into a separate file:

<!--%%if:mockup%%-->
<!--%%mockup-export: "/app/assets/css/login.css" append%%-->
.login-card { padding: 20px; border: 1px solid #ccc; }
<!--%%end%%-->
<!--%%endif%%-->

The if:mockup guard ensures the block is silently skipped during normal processing (no error). In a browser, the <!--%%...%%--> tags are hidden, so the CSS renders inline.

mockup-import reads a file and inserts its content inline. It works as a single tag or as a block tag:

<!--%%mockup-import:/app/assets/css/login.css%%-->

As a block tag, the inline content is kept between the import and end tags. The run command automatically updates this content from the referenced file:

<!--%%mockup-import:/app/assets/css/login.css%%-->
.login-card { padding: 20px; border: 1px solid #ccc; }
<!--%%end%%-->

This keeps mockup files self-contained and browser-renderable while the exported files remain the source of truth.

Indentation: The indent:N flag prepends N spaces to each non-empty line of the imported content. Use indent:Ntab for tabs. This is useful when importing fragments into indented contexts:

<div>
    <section>
        <!--%%mockup-import:"/shared/nav.html" indent:8%%-->
    </section>
</div>

Nesting: mockup-import inside mockup-export works normally (the imported content becomes part of the export). mockup-export inside mockup-import is ignored — imported content is inserted as raw text without parsing.

Quoted paths are supported for filenames with spaces: mockup-export: "/path with spaces/file.css" append

Save-mode cascade: The write mode for mockup-export follows a cascade: <mockup-list save-mode><item save-mode> → tag-level mode (the optional append or overwrite after the path).

touchedFiles behavior: The first write to a given file in a session always truncates (clean start per execution). Subsequent writes respect the mode: append adds content, overwrite replaces it.

Dependency analysis

Mockup files that use mockup-export and mockup-import form a dependency graph. If file A exports to /x.css and file B imports /x.css, then B depends on A and A must be processed first.

Dependencies are resolved at the export block level, not the file level. This means dependencies within the same file are also handled. If a file contains an export block that imports a path produced by another export block later in the same file, the system detects this and processes the blocks in the correct order (multiple passes over the file if needed).

<!--%%mockup-export:/css/combined.css%%-->
  <!--%%mockup-import:/css/base.css%%-->
<!--%%end%%-->
<!--%%mockup-export:/css/base.css%%-->
  .base { margin: 0; }
<!--%%end%%-->

In this example, base.css is exported first (no dependencies), then combined.css is exported and correctly imports the freshly written base.css.

AnalyzeDeps() builds the cross-file dependency graph, detects circular dependencies, and computes the correct processing order via topological sort. Run() calls this automatically and returns an error if cycles are detected.

ms := miniskin.MiniskinNew(contentPath, modulesPath)
dm, err := ms.AnalyzeDeps()
if err != nil {
    log.Fatal(err)
}
fmt.Print(dm.String())

order, err := dm.ProcessingOrder()
// order: ["a_mockup.html", "b_mockup.html", ...] (dependencies first)
Negative templates

Adding negative="filename" to a mockup item generates a reverse template:

<mockup-list>
    <item src="login_mockup.html" negative="login_negative.html" />
</mockup-list>

The transformation replaces all mockup-export:path...end blocks with mockup-import:path...end-mockup-import blocks. This produces a template that imports the exported files instead of containing them inline.

Original mockup:

<!--%%if:mockup%%-->
<!--%%mockup-export:/css/login.css%%-->
.login { padding: 20px; }
<!--%%end%%-->
<!--%%endif%%-->

Generated negative:

<!--%%if:mockup%%-->
<!--%%mockup-import:/css/login.css%%-->
<!--%%end-mockup-import%%-->
<!--%%endif%%-->

Nested mockup-export blocks each produce one mockup-import...end-mockup-import block. All other content (conditionals, variables, etc.) is preserved.

External / Origin

Some projects need to consume an artefact produced by a sibling project (e.g. a UI library kept in its own repo) without checking that artefact into the consuming repo's history. <external> blocks copy such files into place at build time, sourcing them from a per-developer registry.

Registry: miniskin-origin.xml

The registry lives at contentPath root and is per developer: it should be .gitignored so each contributor can point origins at their own local clones / build outputs. The file is optional — only required when <external> blocks exist somewhere in the project.

<miniskin>
  <origin name="closure-ui">
    <local>C:\HD\F\_sams\closure-ui</local>
  </origin>
</miniskin>

<local> is the only supported source: an absolute filesystem path. Each developer is expected to clone and build the sibling project themselves; miniskin only handles the copy. Network sources (<github-release>, <http>) are intentionally not supported — <local> already keeps the projects separated, and adding fetch/cache/auth would invite cross-platform pain (TLS roots, proxies, Windows path quirks, token conventions) for no real gain over the current workflow.

<external> block

Lives in any subdirectory *.miniskin.xml (anywhere a <resource-list> or <mockup-list> could appear). Each <external-item> describes one copy:

<external>
  <external-item origin="closure-ui"
                 src="./release/closure_ui.js"
                 dstfile="./src/app_source.js" />
</external>
Attribute Description
origin name of an entry in miniskin-origin.xml
src path inside the origin's <local> root
dstfile destination relative to the directory of the declaring XML (same convention as <item src>)

Once copied, downstream <item src="./src/app_source.js"> references the file like any other source.

Behaviour
  • Runs as step 0 of Run() (and as a prelude to BuildEmbed()), before dependency analysis, mockup processing, and build embed — so item sources that depend on copied files exist before assembly.
  • Copy is mtime+size-aware: dst is left untouched when it matches the source; otherwise it is overwritten and the source mtime is propagated to dst. A second run is a no-op.
  • Parent directory of dstfile is created if missing.
  • Errors are hard and include absolute paths plus the declaring XML:
    • origin name not found in the registry
    • origin has no <local> (placeholder for future source kinds; currently always an error)
    • source file missing
    • external-item missing the origin attribute
Workflow

Each repo decides its own commit/integration cadence:

  1. Work on the sibling project, build it, commit when ready in its repo.
  2. In the consuming project, run miniskin run — externals refresh automatically based on file mtime.
  3. Commit the copied artefact to the consuming repo when you're ready to ship it.

The miniskin-origin.xml registry stays out of git, so each developer (and CI, if desired) can resolve origins differently.

API

High-level functions
Function Description
MiniskinRun(contentPath, modulesPath, verbosity...) Full pipeline: mockup update + build + codegen
MiniskinGenerate(contentPath, modulesPath, verbosity...) Build embed assets + codegen (no mockup processing)
MiniskinMockupUpdate(contentPath, modulesPath, verbosity...) Deps check + mockup export + refresh imports
TransformNegative(content) Transform a single mockup string into a negative template
CombineDir(dir) Combine subdirectory XMLs into a single XML with nested resource-lists
SplitXML(xmlPath) Split nested resource-lists into separate XMLs per subdirectory
Types
Type Constructor Description
Miniskin MiniskinNew(contentPath, modulesPath) Template processor
Codegen CodegenNew(contentPath, modulesPath) Code generator
DepMap (returned by AnalyzeDeps) Dependency graph with cycle detection

Miniskin methods:

Method Description
Run() Full pipeline: deps + export + update + build
AnalyzeDeps() Build dependency map, detect cycles
UpdateImports() Refresh inline content in mockup-import blocks
ProcessMockupExport() Export only (pass 1)
BuildEmbed() Build only (pass 2)
SetVerbosity(v) Set log detail level
Silent() Disable console output

DepMap methods:

Method Description
ProcessingOrder() Topological sort (dependencies first). Error if cycles exist
HasCycles() Returns true if circular dependencies were detected
String() Human-readable dependency map
Verbosity

Control the level of log detail:

Level Constant Description
0 VerbositySilent No console output
1 VerbosityNormal Phase headers and processed items (default)
2 VerbosityVerbose + dependency analysis, processing order
3 VerbosityDebug + all internal details
ms := miniskin.MiniskinNew(contentPath, modulesPath)
ms.SetVerbosity(miniskin.VerbosityVerbose)

// Or via MiniskinRun:
miniskin.MiniskinRun(contentPath, modulesPath, miniskin.VerbosityVerbose)
Log output

By default, processing steps are logged to stdout. Verbosity controls what is shown.

ms.Silent()                  // disable console output
ms.Output = os.Stderr        // redirect
ms.Output = myWriter         // any io.Writer

If the XML specifies a log file, output is written to both console and file (the log file always receives output regardless of verbosity):

<miniskin log="miniskin.log">
Generated files tracking

Result.GeneratedFiles lists all files created by mockup-export and negative generation, in creation order, with their source:

for _, gf := range result.GeneratedFiles {
    fmt.Printf("%s (from: %s)\n", gf.File, gf.Source)
}

XML format

All configuration uses *.miniskin.xml files with a <miniskin> root element.

Root

The root XML file (in contentPath) declares globals, escape rules, the bucket list, and optionally a log file and skin directory:

<miniskin skin-dir="_skin" log="miniskin.log">
  <globals>
    <var name="appName" value="MyApp" />
  </globals>

  <escape ext="*.html,*.html.tmpl" as="html" />
  <escape ext="*.js,*.js.tmpl" as="js" />

  <bucket-list filename="generated_embed.go" module="content" import="myproject/content"
               template="custom_embed.tmpl">
    <bucket src="app" dst="/modules/app/reqctx/generated_assets.go"
            module-name="reqctx" recurse-folder="all" skin-dir="app/_skin"
            template="custom_bucket.tmpl"
            template-function-map="MyTemplateFuncMap()" />
  </bucket-list>
</miniskin>

<bucket-list> accepts an omit attribute to skip codegen outputs. Values are comma- or space-separated:

Value Effect
embed skip the embed file (Codegen.GenerateEmbed)
module skip per-bucket module files (Codegen.GenerateBucketFile)
<bucket-list omit="embed,module">
  <bucket src=".">
    <resource-list>
      <item type="static,parse" src="./_source.list" file="bundle.js" />
    </resource-list>
  </bucket>
</bucket-list>

When both outputs are omitted, filename and module may be left unset — useful when miniskin is being used to assemble assets for a non-Go project (e.g. a JavaScript bundle).

Subdirectory

Subdirectory *.miniskin.xml files contain one or more <resource-list> elements and/or a <mockup-list>:

<miniskin>
  <resource-list urlbase="/assets" skin-dir="rskins">
    <item type="static" file="app.css" />
    <item type="static" src="combined_src.css" file="combined.css" />
  </resource-list>

  <mockup-list save-mode="overwrite">
    <var name="policybanner" value="1" />
    <item src="mockup_login.html" negative="login_negative.html" save-mode="append">
      <var name="title" value="Login" />
    </item>
  </mockup-list>
</miniskin>

Resource lists can be chained (multiple at the same level) and nested (child resource-lists inside a parent). A nested <resource-list> uses src to set its base directory relative to the parent:

<miniskin>
  <resource-list urlbase="/assets">
    <item type="static" file="app.css" />
  </resource-list>
  <resource-list urlbase="/pages">
    <item type="static" file="index.html" />
    <resource-list src="login" urlbase="/login">
      <item type="static" file="signin.html" />
    </resource-list>
  </resource-list>
</miniskin>

Attributes skin-dir, mux-include, mux-exclude, template-function-map, and <escape> rules cascade from parent to child resource-lists, following the same override pattern used throughout the XML hierarchy.

Items

Each item in a resource list describes a content file:

<item type="html-template,nomux,parse" src="signin_src.html" file="signin.html" key="/login/signin" />
  • file — output filename (what gets embedded)
  • src — source filename (optional; if present, item is processed through the template engine)
  • type — comma-separated flags: static, html-template, nomux, parse, etc.
  • key — logical key for asset lookup
  • url / alt-url-abs — URL routing attributes
  • escape — override default escape type for this item (html, js, url, sql, etc.)
  • template-function-map — Go expression returning template.FuncMap for this item (overrides parent)

If src is absent, file is embedded as-is (no processing).

Mux include/exclude

The mux-include and mux-exclude attributes control which items are registered on the HTTP mux. Items excluded by these patterns automatically receive the nomux flag (they go to the assets map instead of being registered as routes).

These attributes cascade through three levels, each overriding the parent when set:

<miniskin>  →  <bucket-list>  →  <bucket>  →  <resource-list>
Attribute Default Description
mux-include * Comma-separated glob patterns; only matching files are included in the mux
mux-exclude (empty) Comma-separated glob patterns; matching files are excluded from the mux

An item is excluded from the mux (gets nomux added to its type) if:

  • Its filename does not match mux-include, OR
  • Its filename matches mux-exclude

Items with an explicit nomux flag in their type attribute are always excluded regardless of patterns.

Example: include only static assets, exclude templates:

<miniskin mux-include="*.js,*.css,*.png,*.jpg,fav.ico">
  <bucket-list filename="embed.go" module="content">
    <bucket src="app" dst="/gen.go" module-name="app" />
  </bucket-list>
</miniskin>

Equivalent using mux-exclude:

<miniskin mux-exclude="*.html,*.tmpl">
  <bucket-list filename="embed.go" module="content">
    <bucket src="app" dst="/gen.go" module-name="app" />
  </bucket-list>
</miniskin>

Override at a lower level:

<bucket-list mux-exclude="*.html">
  <bucket src="api" dst="/gen.go" module-name="api" mux-exclude="" />
  <!-- api bucket inherits mux-exclude="" only if non-empty; empty = inherit parent -->
</bucket-list>

Patterns use Go's filepath.Match syntax (e.g. *.css, fav.ico, app-*.js).

Template function map

The template-function-map attribute injects a template.FuncMap into parsed templates (items with the parse flag). The value is a Go expression that returns template.FuncMap, typically a function call.

Cascades through three levels, each overriding the parent when set:

<bucket>  →  <resource-list>  →  <item>
<bucket src="app" dst="/gen.go" module-name="app"
        template="miniskin::mux"
        template-function-map="AppFuncMap()">
  <!-- All parsed items in this bucket use AppFuncMap() -->
</bucket>

Override at resource-list or item level:

<resource-list urlbase="/admin" template-function-map="AdminFuncMap()">
  <item type="html-template,nomux,parse" file="page.html" key="/admin/page" />
  <item type="html-template,nomux,parse" file="special.html" key="/admin/special"
        template-function-map="SpecialFuncMap()" />
</resource-list>

The generated code calls .Funcs(expr) before .Parse():

// Without template-function-map:
parsedTemplates["/page"] = template.Must(template.New("/page").Parse(string(content.PageHtml)))

// With template-function-map="AppFuncMap()":
parsedTemplates["/page"] = template.Must(template.New("/page").Funcs(AppFuncMap()).Parse(string(content.PageHtml)))

The function must be defined in the same package as the generated bucket file and must return template.FuncMap.

Generated Go code

Embed file

The embed file (e.g. generated_embed.go) contains one //go:embed directive and []byte variable per item:

package content

import _ "embed"

//go:embed app/assets/app.css
var AppAssetsAppCss []byte

Each variable is a direct pointer to a binary segment — no copy, no decompression.

Bucket file

miniskin includes two built-in bucket templates, selectable via the template attribute:

miniskin::default

Used when no template attribute is specified. Generates an Asset type, an asset slice, and generic lookup/registration functions:

type Asset struct {
    Key  string
    Data []byte
    Mime string
    Type string
}

func Assets() []Asset
func Get(key string) *Asset
func GetParsedTemplate(key string) *template.Template
func StaticFiles() []Asset
func Templates() []Asset
func RegisterRoutes(fn func(url, mime string, data []byte))

Items with the parse flag are pre-parsed as *template.Template at init time. RegisterRoutes calls the callback for each static item not flagged nomux.

miniskin::mux

Generates code that registers routes directly on an *http.ServeMux:

func RegisterRoutes(mux *http.ServeMux, tmplHandlers map[string]http.HandlerFunc)
func GetParsedTemplate(key string) *template.Template
var Templates map[string][]byte

RegisterRoutes registers static files with exact-path matching and wires template routes via the tmplHandlers map.

Usage in XML:

<bucket template="miniskin::mux" ... />

Custom templates

The template attribute on <bucket-list> and <bucket> accepts three forms:

Value Source
(empty) Built-in miniskin::default
miniskin::name Built-in named template
path/to/file.tmpl Custom template file relative to contentPath
<bucket-list template="my_embed.tmpl" ...>
    <bucket template="miniskin::mux" ... />
</bucket-list>
Embed template

Available functions: embedPath, embedVar. Data: full Result struct.

Bucket template

Available functions: embedVar, mimeType, hasFlag, embedPkg, embedImport. Data: BucketList, Bucket, Items.

If a custom embed template is used, custom bucket templates must also be provided (since variable names may differ).

Usage

The simplest way — one function call:

if err := miniskin.MiniskinRun(contentPath, modulesPath); err != nil {
    log.Fatal(err)
}

With verbose output:

if err := miniskin.MiniskinRun(contentPath, modulesPath, miniskin.VerbosityVerbose); err != nil {
    log.Fatal(err)
}

Mockup update only (export + refresh imports):

miniskin.MiniskinMockupUpdate(contentPath, modulesPath)

Generate only (build + codegen, no mockup processing):

miniskin.MiniskinGenerate(contentPath, modulesPath)

Single-file negative transform (no XML needed):

result := miniskin.TransformNegative(content)

For more control, use the types separately:

ms := miniskin.MiniskinNew(contentPath, modulesPath)
ms.SetVerbosity(miniskin.VerbosityVerbose)

result, err := ms.Run()
if err != nil {
    log.Fatal(err) // includes circular dependency errors
}

cg := miniskin.CodegenNew(contentPath, modulesPath)
if err := cg.GenerateAll(result); err != nil {
    log.Fatal(err)
}

Dependency analysis only:

ms := miniskin.MiniskinNew(contentPath, modulesPath)
dm, err := ms.AnalyzeDeps()
if err != nil {
    log.Fatal(err)
}
fmt.Print(dm.String())
order, _ := dm.ProcessingOrder()
for i, src := range order {
    fmt.Printf("%d. %s\n", i+1, src)
}

CLI

miniskin <command> [flags]

Commands:
  run                    Mockup update + Build + Generate code
  generate               Build embed assets + Generate Go code
  generate-claude-skill  Generate Claude Code SKILL.md
  mockup update          Export mockup pieces + Refresh imports
  mockup negative        Transform a mockup file into a negative template
  deps                   Show dependency map and processing order
  combine <dir>          Combine subdirectory XMLs into one
  split <file>           Split nested resource-lists into separate XMLs

Flags:
  -content string        path to content directory (default ".")
  -modules string        path to modules directory (default ".")
  -v                     verbose output (dependency analysis, processing order)
  -vv                    debug output (all internal details)
  -silent                suppress all output

Mockup negative flags:
  -src string            source mockup file (required)
  -dst string            destination negative template file (required)

Generate-claude-skill flags:
  -dst string            destination path (default: .claude/skills/miniskin/SKILL.md)
  -force                 overwrite existing destination file

Examples:

miniskin run
miniskin run -v
miniskin generate
miniskin generate-claude-skill
miniskin generate-claude-skill -dst path/SKILL.md -force
miniskin mockup update
miniskin mockup negative -src mockup_login.html -dst login_negative.html
miniskin deps
miniskin combine content/app
miniskin split content/app/app.miniskin.xml

File structure example

content/
  content.miniskin.xml          # root: globals + bucket-list
  _skin/
    default.html                # skin layout
  app/
    _shared/
      header.html               # include fragment
    assets/
      assets.miniskin.xml       # resource-list for static files
      app.css
      app.js
    login_dialog/
      login.miniskin.xml        # resource-list + mockup-list
      login_mockup.html         # mockup source (mockup-export inside)
      signin_src.html           # source with front-matter + skin
      signin.html               # generated output (gitignored)

AI assistance

The ai/core/ directory contains miniskin's documentation in modular files (overview.md, syntax.md, pipeline.md, examples.md, rules.md). These are the canonical source — both generators below produce their output from them.

Claude Code
miniskin generate-claude-skill

Creates .claude/skills/miniskin/SKILL.md (adds the Claude skill frontmatter and concatenates ai/core/ sources). Override destination with -dst path/SKILL.md, overwrite with -force.

Other agents (Cursor, Aider, Windsurf, AGENTS.md, …)
miniskin generate-agent-docs

Creates AGENTS.md in the current directory — a plain Markdown concatenation of the same sources, without any tool-specific frontmatter. Point it elsewhere with -dst:

miniskin generate-agent-docs -dst .cursor/rules/miniskin.mdc -force
miniskin generate-agent-docs -dst CONVENTIONS.md -force

Suitable for any tool that accepts plain Markdown context.

Background

miniskin originated as a helper for go generate in projects that needed fine-grained control over which assets are embedded and how they are registered. It is not a static site generator — it is an asset assembler. It does not replace tools like Hugo or Jekyll; it operates at a different layer, producing Go source files that compile into the binary.

License

MIT

Documentation

Overview

Package miniskin is a build-time template assembler for Go projects. It processes content files (HTML, CSS, JS) with percent-tag substitution, includes, skins (layouts), and mockup extraction.

Quick start

For the simplest usage, call MiniskinRun to run the full pipeline (mockup update, build, and code generation) in a single call. Use MiniskinGenerate for build + code generation without mockup processing, or MiniskinMockupUpdate for mockup export + import refresh only.

Pipeline

The Miniskin.Run method executes four steps:

  1. Analyze dependencies between mockup files and detect circular references
  2. Process mockup exports (extract content to files via mockup-export directives)
  3. Update imports (refresh inline content in mockup-import blocks)
  4. Build embed (process resource items, resolve variables, apply skins)

Dependency analysis

Use Miniskin.AnalyzeDeps to inspect the dependency graph between mockup files. The returned DepMap provides DepMap.ProcessingOrder for topological ordering and DepMap.HasCycles for circular dependency detection.

Verbosity

Control log detail with Miniskin.SetVerbosity or Miniskin.Silent. VerbosityNormal logs phase headers, VerbosityVerbose adds dependency analysis and processing order, VerbosityDebug logs all internal details.

Types

For finer control, use MiniskinNew to create a Miniskin instance for template processing, and CodegenNew to create a Codegen instance for Go source file generation.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CombineDir added in v0.3.4

func CombineDir(targetDir string) error

CombineDir reads all *.miniskin.xml files under targetDir recursively, builds a nested structure, and writes a single combined XML at targetDir. The old XML files in subdirectories are removed.

func GenerateAgentDocs added in v0.3.4

func GenerateAgentDocs() string

GenerateAgentDocs produces an agent-agnostic Markdown document concatenating the embedded ai/core sources without any tool-specific frontmatter. Suitable for AGENTS.md, .cursor/rules, CONVENTIONS.md, or any LLM context input.

func GenerateSkill added in v0.3.0

func GenerateSkill() (string, error)

GenerateSkill produces the Claude Code SKILL.md content from embedded sources.

func MiniskinGenerate added in v0.3.0

func MiniskinGenerate(contentPath, modulesPath string, verbosity ...Verbosity) error

MiniskinGenerate builds embed assets and generates Go source code. Does not process mockups — use [MiniskinFull] for the complete pipeline.

func MiniskinMockupClean added in v0.3.4

func MiniskinMockupClean(contentPath, modulesPath string, verbosity ...Verbosity) error

MiniskinMockupClean empties the inline content of mockup-import blocks.

func MiniskinMockupUpdate added in v0.3.0

func MiniskinMockupUpdate(contentPath, modulesPath string, verbosity ...Verbosity) error

MiniskinMockupUpdate processes mockup exports and refreshes imports. Runs dependency analysis, mockup-export extraction, and import refresh.

func MiniskinRun added in v0.3.0

func MiniskinRun(contentPath, modulesPath string, verbosity ...Verbosity) error

MiniskinRun runs the full pipeline: mockup update + build + code generation. Pass VerbosityNormal for standard output, or higher levels for more detail.

func SplitXML added in v0.3.4

func SplitXML(xmlPath string) error

SplitXML reads a combined XML file and splits nested resource-lists into separate *.miniskin.xml files per subdirectory.

func TransformNegative added in v0.3.0

func TransformNegative(content string) string

TransformNegative replaces mockup-export...end blocks with mockup-import tags. Content between export and end is removed. Nested exports each produce an import tag. All import tags are emitted in <!--%%...%%--> syntax regardless of the original.

Types

type Bucket

type Bucket struct {
	Src                 string
	Dst                 string
	ModuleName          string
	Template            string // custom template file for bucket generation
	TemplateFunctionMap string // expression for template.FuncMap (e.g. "MyFuncMap()")
	// contains filtered or unexported fields
}

Bucket represents a content bucket.

type BucketList

type BucketList struct {
	Filename    string
	Module      string
	Import      string
	Template    string // custom template file for embed generation
	ProjectRoot string // project root relative to contentPath (for resolving dst)
	Omit        string // comma/space-separated codegen outputs to skip ("embed", "module")
	Buckets     []Bucket
}

BucketList holds the embed generation config from the root miniskin.xml.

func (BucketList) OmitsCodegen added in v0.3.6

func (bl BucketList) OmitsCodegen(key string) bool

OmitsCodegen reports whether the given codegen output is listed in the Omit attribute. Recognised keys: "embed" (skip the embed file), "module" (skip per-bucket module files).

type BucketResult

type BucketResult struct {
	Bucket Bucket
	Items  []Item
}

BucketResult holds a parsed bucket and its collected items.

type Codegen added in v0.3.0

type Codegen struct {
	// contains filtered or unexported fields
}

Codegen generates Go source files with //go:embed directives from a Result produced by Miniskin.Run or Miniskin.BuildEmbed.

func CodegenNew added in v0.3.0

func CodegenNew(contentPath, modulesPath string) *Codegen

CodegenNew creates a Codegen instance. contentPath: root directory where content files live. modulesPath: root directory where Go modules live.

func (*Codegen) GenerateAll added in v0.3.0

func (cg *Codegen) GenerateAll(result *Result) error

GenerateAll writes the embed file and all bucket files. Outputs listed in the bucket-list "omit" attribute are skipped: "embed" → skip the embed file, "module" → skip per-bucket module files.

func (*Codegen) GenerateBucketFile added in v0.3.0

func (cg *Codegen) GenerateBucketFile(result *Result, br BucketResult) error

GenerateBucketFile writes the generated Go file for a single bucket. Uses the custom template from the bucket if set, otherwise the built-in default.

func (*Codegen) GenerateEmbed added in v0.3.0

func (cg *Codegen) GenerateEmbed(result *Result) error

GenerateEmbed writes the embed file (e.g. generated_embed.go) with //go:embed directives. Uses the custom template from bucket-list if set, otherwise the built-in default.

type DepEdge added in v0.3.0

type DepEdge struct {
	Source string // mockup source file (relative to contentPath)
	Target string // exported/imported file path
	Kind   string // "export" or "import"
}

DepEdge represents a dependency between a mockup source file and a target file.

type DepMap added in v0.3.0

type DepMap struct {
	Edges  []DepEdge
	Cycles [][]string // circular dependency chains
}

DepMap holds the dependency graph for mockup-export/mockup-import directives.

func (*DepMap) HasCycles added in v0.3.0

func (dm *DepMap) HasCycles() bool

HasCycles returns true if any circular dependencies were detected.

func (*DepMap) ProcessingOrder added in v0.3.0

func (dm *DepMap) ProcessingOrder() ([]string, error)

ProcessingOrder returns mockup source files sorted so that dependencies are processed before the files that import them. Returns an error if the graph contains cycles.

func (*DepMap) String added in v0.3.0

func (dm *DepMap) String() string

String returns a human-readable representation of the dependency map.

type GeneratedFile added in v0.3.0

type GeneratedFile struct {
	File   string // output path relative to contentPath
	Source string // source file that triggered the generation
}

GeneratedFile records a file created by mockup-export.

type Item

type Item struct {
	Type                string
	File                string
	Src                 string // source file; if set, item needs processing
	URL                 string
	AltURL              string
	Key                 string
	Index               int    // position in the global embed list
	EmbedPath           string // relative path for go:embed, computed during processing
	XMLSrc              string // absolute path of the .miniskin.xml that declared this item
	XMLLine             int    // line number in XMLSrc where this item is declared
	TemplateFunctionMap string // cascaded expression for template.FuncMap
	// contains filtered or unexported fields
}

Item represents a resource item from a miniskin.xml.

func (*Item) HasFlag

func (it *Item) HasFlag(flag string) bool

HasFlag returns true if the item's Type contains the given flag.

func (*Item) NeedsProcessing

func (it *Item) NeedsProcessing() bool

NeedsProcessing returns true if the item has a src attribute.

func (*Item) RouteURL

func (it *Item) RouteURL() string

RouteURL returns the URL for serving this item. Uses Key if set, otherwise URLBase + "/" + File.

type Miniskin

type Miniskin struct {
	Output    io.Writer // log destination; nil = silent
	Verbosity Verbosity // log detail level (default: VerbosityNormal)
	// contains filtered or unexported fields
}

Miniskin is a build-time template assembler. It resolves percent tags and applies skins to produce final files ready for embedding.

func MiniskinNew added in v0.3.0

func MiniskinNew(contentPath, modulesPath string) *Miniskin

MiniskinNew creates a Miniskin instance. contentPath: root directory where source files live. modulesPath: root directory where Go modules live.

func (*Miniskin) AnalyzeDeps added in v0.3.0

func (ms *Miniskin) AnalyzeDeps() (*DepMap, error)

AnalyzeDeps walks all mockup files and builds a dependency map. Detects circular dependencies between mockup sources via export/import chains.

func (*Miniskin) BuildEmbed added in v0.3.0

func (ms *Miniskin) BuildEmbed() (*Result, error)

BuildEmbed collects and processes all resource items across all buckets. Resolves variables, includes, skins, and writes output files. Externals are resolved first so item sources that depend on them exist.

func (*Miniskin) CleanImports added in v0.3.4

func (ms *Miniskin) CleanImports() error

CleanImports walks all mockup source files and removes the inline content of mockup-import blocks, leaving the import and end tags with nothing between them.

func (*Miniskin) ProcessMockupExport added in v0.3.0

func (ms *Miniskin) ProcessMockupExport() (*Result, error)

ProcessMockupExport processes all mockup-lists across all buckets. Only mockup-export side effects matter; the output is discarded. Variables are not resolved — only conditionals and mockup-export are processed.

func (*Miniskin) ProcessNegatives added in v0.3.0

func (ms *Miniskin) ProcessNegatives() (*Result, error)

ProcessNegatives generates only negative templates across all buckets. No mockup-export extraction is performed.

func (*Miniskin) Run

func (ms *Miniskin) Run() (*Result, error)

Run executes the full pipeline:

  1. Resolve <external> blocks (copy files from declared origins)
  2. Analyze dependencies and check for circular references
  3. Process mockup exports (writes extracted files to disk)
  4. Update imports (refresh inline content in mockup-import blocks)
  5. Build embed (process resource items)

func (*Miniskin) SetVerbosity added in v0.3.0

func (ms *Miniskin) SetVerbosity(v Verbosity) *Miniskin

SetVerbosity sets the log detail level.

func (*Miniskin) Silent added in v0.3.0

func (ms *Miniskin) Silent() *Miniskin

Silent disables log output.

func (*Miniskin) UpdateImports added in v0.3.0

func (ms *Miniskin) UpdateImports() error

UpdateImports walks all mockup source files and refreshes the inline content of mockup-import blocks. Single import tags are promoted to block tags (import + content + end). Existing blocks get their content replaced.

type Origin added in v0.3.9

type Origin struct {
	Name  string
	Local string
}

Origin describes a resolved external source declared in miniskin-origin.xml. MVP supports only Local (filesystem path); github-release and others come later.

type Result

type Result struct {
	BucketList     BucketList
	Buckets        []BucketResult
	GeneratedFiles []GeneratedFile
}

Result holds everything miniskin parsed and processed.

type Verbosity added in v0.3.0

type Verbosity int

Verbosity controls the level of detail in log output.

const (
	// VerbositySilent disables all output.
	VerbositySilent Verbosity = 0
	// VerbosityNormal logs phase headers and processed items.
	VerbosityNormal Verbosity = 1
	// VerbosityVerbose also logs dependency analysis and processing order.
	VerbosityVerbose Verbosity = 2
	// VerbosityDebug logs everything including internal details.
	VerbosityDebug Verbosity = 3
)

Directories

Path Synopsis
cmd
miniskin command
Command miniskin is the CLI for the miniskin build-time template assembler.
Command miniskin is the CLI for the miniskin build-time template assembler.

Jump to

Keyboard shortcuts

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