miniskin

package module
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 31 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/pablo-botella/miniskin/cmd/miniskin@latest

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

go get github.com/pablo-botella/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 and mockup-export)
end-if Close if block (specific alias)
end-mockup-export Close mockup-export block (specific alias)
end-mockup-import Close mockup-import block (mandatory — generic end is not valid here)
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, response, 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.

Items flagged response are registered via serveResponse, which replays a canned HTTP response embedded as a raw .http file (status line, optional headers, blank line, optional body). Use it for redirects (3xx + Location), bare statuses (404, 410, …), and static error pages — the route comes from key:

<item type="response" file="old-page.http" key="/old-page/" />
301 Moved Permanently
Location: https://www.example.com/products

The bytes are fixed at build time, like static; declared headers are sent verbatim (net/http still adds Date/Content-Length and sniffs Content-Type when omitted). Anything dynamic belongs in your own mux.HandleFunc.

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-agent-docs  -generate-readme
                         Write/install the composed docs (mkskill family)
  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 family flags:
  -dst string            destination path   -global   install skill under ~/.claude
  -force                 overwrite existing destination file

Examples:

miniskin run
miniskin run -v
miniskin generate
miniskin -generate-claude-skill -global -force
miniskin -generate-agent-docs -dst CONVENTIONS.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 _mkskill/src/ directory contains miniskin's documentation in modular sections — the canonical source. mkskill composes every view from them (this README, AGENTS.md and the Claude skill), and the binary carries the composed docs embedded, so it hands them out by itself.

Claude Code
miniskin -generate-claude-skill -global

Installs ~/.claude/skills/miniskin/SKILL.md (available from every project). Without -global it writes the project-local .claude/skills/miniskin/SKILL.md; -dst picks any other path, -force overwrites.

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

Writes AGENTS.md in the current directory — the same content 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

cargoparse.go is the cargoxml side of the catalog: each element is a consumer that claims what miniskin models and keeps everything foreign in its cargo — *.miniskin.xml files can carry other tools' data (the mkskill piggyback) and survive rewrites whole, comments included. It replaces xmlparse.go's encoding/xml structs one element at a time; both live side by side until the swap.

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

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 DebugCatalogs

func DebugCatalogs(content, echoDst, xrayDst string) error

DebugCatalogs parses every *.miniskin.xml under content with the cargo path and writes the chosen outputs: echo is the byte-faithful rewrite (the preservation, live — and the migration's regression mirror: the parse gets more typed underneath, the echo must never change), rx the claimed-vs-cargo report (the progress map). Destinations are a file path or "-" for stdout; with neither given, the rx goes to stdout.

func MiniskinGenerate

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

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

MiniskinMockupClean empties the inline content of mockup-import blocks.

func MiniskinMockupUpdate

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

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

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

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 BlobIndex

type BlobIndex struct {
	Name   string // blob name (matches blob-name / the data file stem)
	Base   string // URL base the blob is served under (from urlbase)
	ID     string // guid sync token
	Attach string // blob-attach value (assets/mux/templates/* or "")
}

BlobIndex is what codegen needs to wire a blob into the generated binary. The entries themselves live in the blob file (read at runtime by the blob package), so only the identity/wiring is carried here.

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)
	BlobOut     string // output dir for .blob files (relative to project-root), default "blob"
	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

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      // items embedded + served normally (blob items excluded)
	Blobs  []BlobIndex // blobs packed for this bucket (from blob-name resource-lists)
}

BucketResult holds a parsed bucket and its collected items.

type Codegen

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

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

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

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

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

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

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

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

func (*DepMap) HasCycles

func (dm *DepMap) HasCycles() bool

HasCycles returns true if any circular dependencies were detected.

func (*DepMap) ProcessingOrder

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

func (dm *DepMap) String() string

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

type GeneratedFile

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
	BlobName            string // if set, item is packed into this .blob instead of embedded
	PreserveBlobID      string // preserve-blob-if-id: trust the existing blob iff its guid equals this
	BlobAttach          string // blob-attach: which exe structures to wire the blob into (assets/mux/templates/*)
	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

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

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

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

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

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

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

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

SetVerbosity sets the log detail level.

func (*Miniskin) Silent

func (ms *Miniskin) Silent() *Miniskin

Silent disables log output.

func (*Miniskin) UpdateImports

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

type Origin struct {
	Name   string
	Local  *OriginSource
	Remote *OriginSource
}

Origin is a named source with a Local side (a directory on this machine) and/or a Remote side (a base URL). Resolution is per resource id; Local wins over Remote.

type OriginResource

type OriginResource struct {
	ID         string
	Src        string
	Sha256     string
	IgnoreHash bool
}

OriginResource is one offered resource. Src is the file name under the source's base (id defaults to it). Sha256, set on a remote resource, is the author's "contract"; IgnoreHash skips verification (local dev builds).

type OriginSource

type OriginSource struct {
	Base      string
	Resources map[string]OriginResource
}

OriginSource is one side of an origin: a base plus resources keyed by id.

type Result

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

Result holds everything miniskin parsed and processed.

type Verbosity

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