On this page
The helpers the engine packages rest on: frontmatter parsing, atomic writes, Python-compatible JSON encoding, and the string, project and date utilities.
#internal/util
#internal/util
Package util holds the small shared helpers the rest of selfdoc builds on: frontmatter parsing, project manifest and version detection, HTML escaping, path joining, date formatting, title casing, and the Python-compatible string, number and JSON spellings the emitted documents are pinned to.
Nothing here knows about a build, a page or a directive. A helper earns its place here by being needed in more than one package and by having no dependency on any other package of this module.
#Fence
const Fence = "+++"Fence opens and closes a frontmatter block. Everything between the opening fence line and the next line that is exactly the fence is TOML.
#RetiredFence
const RetiredFence = "---"RetiredFence opened the hand-parsed block selfdoc read before the TOML format. A document still opening with it is refused by name, with the converter that rewrites it -- there is no second reader and no fallback.
#ConverterScript
const ConverterScript = "scripts/convert-frontmatter-to-toml.py"ConverterScript is the dry-run-capable script that rewrites a retired block into a TOML one. Every refusal of a retired block names it.
#KindPage
const KindPage Kind = "page"KindPage is a page under a project's docs directory. Every key is optional.
#KindPost
const KindPost Kind = "post"KindPost is a post under a project's posts directory. A post must carry a title, a date and a directive declaration.
#UnknownField
const UnknownField = "unknown"UnknownField is the value [ReadProjectField] returns when no manifest answers -- the sentinel string the Python surface returned, which templates and pages render verbatim.
#PythonSpaceChars
const PythonSpaceChars = `\t\n\v\f\r \x{001c}-\x{001f}\x{0085}\x{00a0}\x{1680}\x{2000}-\x{200a}\x{2028}\x{2029}\x{202f}\x{205f}\x{3000}`PythonSpaceChars are the members of Python's \s, without the enclosing brackets: the ASCII whitespace characters, the four ASCII information separators, and the Unicode whitespace code points. A pattern that composes whitespace with further characters into one class needs the members, which a nested class cannot express.
#PythonWordChars
const PythonWordChars = `\p{L}\p{N}_`PythonWordChars are the members of Python's \w where it matches an identifier character, without the enclosing brackets: a letter, a digit or an underscore, in any script.
#PythonSpaceClass
const PythonSpaceClass = "[" + PythonSpaceChars + "]"PythonSpaceClass is Python's \s.
#PythonNonSpaceClass
const PythonNonSpaceClass = "[^" + PythonSpaceChars + "]"PythonNonSpaceClass is Python's \S, the complement of [PythonSpaceClass].
#PythonWordClass
const PythonWordClass = "[" + PythonWordChars + "]"PythonWordClass is Python's \w where it matches an identifier character.
#GeneratedBy
const GeneratedBy = "0.2.5"GeneratedBy is the strictspec release that produced this file. The runtime pairing guard hard-errors unless it matches the linked runtime exactly.
#SchemaFormatVersion
const SchemaFormatVersion = 1SchemaFormatVersion is the document format_version this validator accepts.
#Frontmatter
type Frontmatter = map[string]anyFrontmatter is the read surface of a parsed frontmatter block: the declared keys mapped to their values.
A value is one of string, bool, int64, float64 or []string -- the Go spellings of the lexeme classes the frontmatter schema declares. A date is carried as the string it was written as ("2026-09-14"), so every reader of a date reads a string; the WRITE surface distinguishes it (see [FrontmatterField] and [FrontmatterDate]).
#Kind
type Kind stringKind is the document kind a block is read as. It selects which of the two kinds the frontmatter schema validates the block against, and it is supplied by the call site rather than written by an author: the docs walk reads pages, the post discovery reads posts.
#FrontmatterDate
type FrontmatterDate stringFrontmatterDate is a frontmatter date value on the WRITE surface: it renders as a bare TOML local date rather than a quoted string.
The read surface carries a date as a plain string, because every reader of a date wants its text. This type exists so a block read and written back out keeps the spelling it had.
#FrontmatterField
type FrontmatterField structFrontmatterField is one key and its value, in the order the block writes it.
Value is one of string, bool, int64, float64, []string or [FrontmatterDate]; [RenderFrontmatter] refuses anything else rather than guessing a spelling.
#Block
type Block structBlock is a parsed frontmatter block and the body behind it.
#FrontmatterDiagnostic
type FrontmatterDiagnostic structFrontmatterDiagnostic is one refusal of a frontmatter block.
#FrontmatterError
type FrontmatterError structFrontmatterError is a block a document may not carry.
#FrontmatterKeyRow
type FrontmatterKeyRow structFrontmatterKeyRow is one row of the frontmatter key registry as the documentation renders it.
#FrontmatterDocument
type FrontmatterDocument structFrontmatterDocument is the frozen typed binding of the "FrontmatterDocument" record. Fields are immutable by convention (shallow-plus-generated-immutability); use With* for copy-on-write.
#SplitFrontmatter
func SplitFrontmatter(text, source string) (block, body string, consumed int, err error)SplitFrontmatter splits a document into its raw frontmatter block and its body, without reading either.
This is the whole of the fence handling, and the only place that knows what a fence looks like: [ReadFrontmatter] validates what this returns, and a caller that wants nothing but the body -- the staleness pass hashing a page's own text -- calls this directly instead of carrying a second reading of the same fence.
A document whose first line is not the fence carries no block: the block is empty, the body is the document, and consumed is zero. A document that opens the fence and never closes it is refused, as is one opening the retired fence. source names the document in every refusal.
#StripFrontmatter
func StripFrontmatter(text, source string) (string, error)StripFrontmatter returns a document's body: everything behind its frontmatter block.
#ReadFrontmatter
func ReadFrontmatter(text, source string, kind Kind) (Block, error)ReadFrontmatter reads a document's frontmatter block as the given kind.
The block is TOML, validated against the frontmatter schema by the generated validator: every key it may carry is declared there, an undeclared key is refused rather than ignored, and each value's lexeme class is the declared one. A post's title, date and directive declaration are required by the same schema, so the surfaces that report a missing one read the verdict here rather than checking the fact again.
source names the document in every refusal.
#RenderFrontmatter
func RenderFrontmatter(fields []FrontmatterField) (string, error)RenderFrontmatter renders fields as a fenced TOML frontmatter block, terminated by a newline.
Every value is spelled by go-toml-edit's own renderers, so a key or a string this writes escapes the way the library that reads it escapes. A value of a type the frontmatter schema declares no lexeme class for is refused rather than guessed at.
#ParsePythonInt
func ParsePythonInt(s string) (int64, bool)ParsePythonInt parses s the way Python's int() does for a base-10 string, reporting whether it is a valid integer literal. Underscores between digits are accepted, as Python accepts them; a hexadecimal or octal prefix is not.
One bound Python does not have: a literal outside the int64 range is reported invalid, where Python's arbitrary-precision int would accept it.
#ParsePythonFloat
func ParsePythonFloat(s string) (float64, bool)ParsePythonFloat parses s the way Python's float() does, reporting whether it is a valid float literal. It accepts underscores between digits and the signed spellings of inf, infinity and nan, and rejects the hexadecimal float syntax Go's own parser would otherwise accept.
#FrontmatterKeyRows
func FrontmatterKeyRows() ([]FrontmatterKeyRow, error)FrontmatterKeyRows answers the key registry, in the order the schema declares it, with the reader-supplied trailer left out: the two keys it carries are the reader's and never an author's, so they belong in prose rather than in a table of what a page may write.
The rows are DERIVED from the schema the generated validator embeds, so the documentation's table and the validator cannot disagree about which keys exist, what type each carries, or which ones a post must have.
#RenderFrontmatterKeyTable
func RenderFrontmatterKeyTable() (string, error)RenderFrontmatterKeyTable renders the key registry as the Markdown table the documentation carries.
#DetectProjectVersion
func DetectProjectVersion(baseDir, fallback string) stringDetectProjectVersion reads the project's version out of its manifest files.
A source language declared in baseDir's selfdoc.json picks the manifest the version is read from (go -> VERSION, python -> pyproject.toml, js/node -> package.json), so a polyglot repository's incidental manifests -- a private browser-test harness's package.json at the root of a Go project, whose version field is conventionally 0.0.0 -- cannot win. That is the version counterpart of the rule [ReadProjectField] applies to the project name.
Without a declaration, or when the picked manifest is absent or carries no version, the original lookup chain applies: pyproject.toml's [project].version, then package.json's "version", then a plain-text VERSION file. It returns fallback when no version is found.
#ReadProjectField
func ReadProjectField(baseDir, field string) stringReadProjectField reads a project metadata field from the project's manifest.
A source language declared in baseDir's selfdoc.json picks the manifest (go -> go.mod, python -> pyproject.toml, js/node -> package.json), so a polyglot repository's incidental manifests cannot win. Without a declaration, or when the picked manifest is absent or unreadable, the original lookup chain applies: pyproject.toml, then package.json, then go.mod.
The "version" field is answered by [DetectProjectVersion] with [UnknownField] as the fallback. Every other unanswerable field is [UnknownField] too -- this surface reports absence in band, as the templates consuming it expect, and never as an error.
#IsPythonSpace
func IsPythonSpace(r rune) boolIsPythonSpace reports whether r is whitespace by Python's str.isspace rule, the predicate str.strip and str.split use.
It is [unicode.IsSpace] plus the four information separators U+001C through U+001F, which Python counts as whitespace and Go does not.
#PythonStrip
func PythonStrip(s string) string { return strings.TrimFunc(s, IsPythonSpace) }PythonStrip reproduces Python's str.strip() with no argument: both ends trimmed of every character [IsPythonSpace] accepts.
[strings.TrimSpace] is close but not the same -- it trims what [unicode.IsSpace] accepts, which omits the four information separators Python trims.
#PythonLStrip
func PythonLStrip(s string) string { return strings.TrimLeftFunc(s, IsPythonSpace) }PythonLStrip reproduces Python's str.lstrip() with no argument.
#PythonRStrip
func PythonRStrip(s string) string { return strings.TrimRightFunc(s, IsPythonSpace) }PythonRStrip reproduces Python's str.rstrip() with no argument.
#PythonFields
func PythonFields(s string) []string { return strings.FieldsFunc(s, IsPythonSpace) }PythonFields reproduces Python's str.split() with no argument: s split on runs of [IsPythonSpace] runes, with no empty items, so leading and trailing whitespace contribute nothing.
#PythonSplitLines
func PythonSplitLines(text string) []stringPythonSplitLines splits text the way Python's str.splitlines() does: on every character in pythonLineBreaks, counting "\r\n" once, and with a trailing terminator producing no final empty line.
#PythonRepr
func PythonRepr(v any) stringPythonRepr renders v the way Python's repr() does -- the spelling every ported diagnostic that interpolates {value!r} was written against.
A string is quoted by [pythonReprString]. A mapping's keys are rendered in sorted order rather than insertion order: Go maps carry no insertion order, and a diagnostic that names an object has to be reproducible. A value of a type Python has no counterpart for falls back to [fmt.Sprint].
#PythonStr
func PythonStr(v any) stringPythonStr renders v the way Python's str() -- and therefore an f-string interpolation -- renders it.
A container renders through [fmt.Sprint] rather than Python's own container repr, which is what every ported call site did: the containers that reach a str() interpolation are rejected by a type check before any message could quote one, so the difference cannot reach a document. A value that really is a container to be shown goes through [PythonRepr].
#PythonStrOrEmpty
func PythonStrOrEmpty(v any) stringPythonStrOrEmpty renders v the way Python's str(value or "") idiom does: a falsy value -- an absent key, a null, a false, a zero, an empty string, an empty container -- becomes the empty string, and anything else becomes its [PythonStr] rendering.
Every optional string key of a config or a frontmatter block was read through that idiom, so an absent key and a declared empty one are the same answer.
#PythonTypeName
func PythonTypeName(v any) stringPythonTypeName renders the name Python's type(value).__name__ gives a decoded JSON or TOML value, for a refusal that reports the type it was handed instead of the one it wanted.
#PythonJSON
func PythonJSON(v any) ([]byte, error)PythonJSON encodes v the way Python's json.dumps(v, sort_keys=True, separators=(",", ":")) does, byte for byte.
That exact spelling is the hash-store's schema-hash input, so a divergence here silently invalidates every stored hash. The reproduced rules are:
- No whitespace anywhere: "," between items, ":" between a key and its value. - Object keys sorted by code point. Only string keys are accepted, because Python's sort_keys refuses a mixed-type key set. - ensure_ascii: every character outside printable ASCII is escaped as \uXXXX in lowercase hex, with a surrogate pair above the Basic Multilingual Plane. "<", ">" and "&" are NOT escaped -- Python's encoder does no HTML escaping, unlike Go's encoding/json. - Floats render as Python's repr does: shortest round-trip digits, a trailing ".0" on an integral value, and exponential notation only when the decimal point sits at or below position -4 or above position 16. Infinities and NaN render as Python's non-standard Infinity, -Infinity and NaN literals, which json.dumps emits by default.
A nil slice encodes as "[]" and a nil map as "{}" -- Python has no nil collection, so a Go port's unset slice stands for the empty list the Python it replaces would have built. Only a nil interface or nil pointer is "null".
#PythonJSONIndent2
func PythonJSONIndent2(v any) ([]byte, error)PythonJSONIndent2 encodes v the way Python's json.dumps(v, indent=2, sort_keys=True) does, byte for byte -- the spelling the hash store's own file is written with.
Every rule of [PythonJSON] holds, except that an indent switches Python's separators to "," plus a newline and ": " between a key and its value. An empty object and an empty array still render as "{}" and "[]".
#PythonJSONString
func PythonJSONString(s string) stringPythonJSONString quotes s the way Python's json encoder does under ensure_ascii: the short escapes for backslash, quote, backspace, form feed, newline, carriage return and tab, a \uXXXX escape for every other character outside the printable ASCII range 0x20-0x7E, and a surrogate pair for a code point above the Basic Multilingual Plane.
#PythonFloatRepr
func PythonFloatRepr(f float64) stringPythonFloatRepr renders f the way Python's repr(float) does, which is also what json.dumps writes for a float.
The digits are the shortest decimal string that round-trips. Notation is chosen from the decimal point's position: exponential when it sits at or below -4 or above 16, fixed otherwise, and a fixed rendering always carries a fractional part (so 1.0 is "1.0", never "1"). Infinities and NaN render as json.dumps's non-standard Infinity, -Infinity and NaN literals.
#ValidateBytes
func ValidateBytes(input []byte, syntax string) (*FrontmatterDocument, []strictspec.Diagnostic)ValidateBytes is the raw-bytes entry point: lossless parse of input in the given syntax ("json" | "toml" | "jsonl"), then validate. It returns the typed root value (nil when any diagnostic fired) and the ordered diagnostics.
#ValidateValue
func ValidateValue(v strictspec.Value) (*FrontmatterDocument, []strictspec.Diagnostic)ValidateValue is the tagged-value entry point: validate an already-parsed tagged document value (from strictspec.LoadValue or a typed constructor).
#ValidateBytesWithEvidence
func ValidateBytesWithEvidence(input []byte, syntax string, evidence map[string][]map[string]any) (*FrontmatterDocument, []strictspec.Diagnostic)ValidateBytesWithEvidence is ValidateBytes plus cross-document resolver evidence for the phase-2 constraint vocabulary.
#EscapeHTML
func EscapeHTML(s string) stringEscapeHTML escapes s for insertion into HTML text or a double-quoted attribute value: "&", "<", ">" and the double quote, in that order.
The apostrophe is deliberately NOT escaped, so the output matches Python's html.escape(s, quote=True) rather than Go's html.EscapeString, which also rewrites "'" to "'" and would change every rendered page.
#FormatDateLong
func FormatDateLong(t time.Time) stringFormatDateLong renders t as Python's strftime("%B %-d, %Y") does in the C locale: the English month name, the day of the month without a leading zero, a comma, and the four-digit year -- "September 1, 2026".
#ResolveDirectivePath
func ResolveDirectivePath(baseDir, path string) stringResolveDirectivePath resolves a directive's path attribute against the project's base directory.
This is the one place filesystem directive paths are resolved, so every directive that reads a path attribute behaves identically and any future normalization or sandboxing has a single home.
#PathJoin
func PathJoin(parts ...string) stringPathJoin reproduces Python's posixpath.join, which is what every ported call site was written against.
It differs from [path/filepath.Join] in two ways that reach real documents: an absolute later element REPLACES everything before it instead of being appended, and the result is never cleaned, so "docs" joined with "../x" stays "docs/../x" rather than collapsing to "x". Use [path/filepath.Join] for a new path that no Python call site constrains.
#TitleCase
func TitleCase(s string) stringTitleCase reproduces Python's str.title().
Every cased character that follows an uncased one is title-cased and every other cased character is lower-cased, with "cased" meaning the Unicode Cased property -- not "alphabetic". An apostrophe is uncased, so "don't" becomes "Don'T", and a digit is uncased too, so "a1b" becomes "A1B". Both are the documented behavior of the Python method this replaces, not accidents.
The full (multi-character) case mappings Python applies are reproduced from the tables below, so a word-initial sharp s becomes "Ss" and a word-initial ff ligature becomes "Ff" as they do in Python, rather than staying put the way the single-rune unicode.ToTitle would leave them.
#DecodeTOML
func DecodeTOML(data []byte) (map[string]any, error)DecodeTOML decodes a TOML document into the generic Go value every caller in this module validates by hand.
The shapes are the ones the hand-written validations and their pinned refusals were written against: a string, an int64, a float64, a bool, a time.Time for each of the four date-time flavors, a []any for an array, a map[string]any for a table, a []map[string]any for an array of tables, and nested maps for a dotted key. A caller reads them with a type assertion and refuses anything else by name, so a shape stated here is part of every one of those refusals.
#DecodeTOMLFile
func DecodeTOMLFile(path string) (map[string]any, error)DecodeTOMLFile reads path and decodes it through [DecodeTOML].
#DecodeTOMLOrdered
func DecodeTOMLOrdered(data []byte) (map[string]any, [][]string, error)DecodeTOMLOrdered decodes like [DecodeTOML] and additionally answers every key the document declares, as its path from the root, in document order.
A Go map has no order, so a renderer whose row order is document order reads the order from here instead. The list carries a table header before the keys written under it, one entry per array-of-tables element, and the keys of an inline table under the key it is bound to -- a sequence a caller replays over the decoded values to rebuild the document's own order.
#Block.Keys
func (b Block) Keys() []stringKeys returns the block's keys in the order it wrote them.
#FrontmatterError.Error
func (e *FrontmatterError) Error() string#FrontmatterError.Names
func (e *FrontmatterError) Names(field string) boolNames reports whether any diagnostic is about the given key.
#FrontmatterError.HasCode
func (e *FrontmatterError) HasCode(code string) boolHasCode reports whether any diagnostic carries the given validator code.
#FrontmatterDocument.WithFormatVersion
func (x *FrontmatterDocument) WithFormatVersion(v int64) *FrontmatterDocumentWithFormatVersion returns a copy of FrontmatterDocument with FormatVersion set to the given value.
#FrontmatterDocument.WithDocumentKind
func (x *FrontmatterDocument) WithDocumentKind(v string) *FrontmatterDocumentWithDocumentKind returns a copy of FrontmatterDocument with DocumentKind set to the given value.
#FrontmatterDocument.WithTitle
func (x *FrontmatterDocument) WithTitle(v string) *FrontmatterDocumentWithTitle returns a copy of FrontmatterDocument with Title set to the given value.
#FrontmatterDocument.WithDescription
func (x *FrontmatterDocument) WithDescription(v string) *FrontmatterDocumentWithDescription returns a copy of FrontmatterDocument with Description set to the given value.
#FrontmatterDocument.WithSlug
func (x *FrontmatterDocument) WithSlug(v string) *FrontmatterDocumentWithSlug returns a copy of FrontmatterDocument with Slug set to the given value.
#FrontmatterDocument.WithTags
func (x *FrontmatterDocument) WithTags(v []string) *FrontmatterDocumentWithTags returns a copy of FrontmatterDocument with Tags set to the given value.
#FrontmatterDocument.WithDate
func (x *FrontmatterDocument) WithDate(v string) *FrontmatterDocumentWithDate returns a copy of FrontmatterDocument with Date set to the given value.
#FrontmatterDocument.WithUpdated
func (x *FrontmatterDocument) WithUpdated(v string) *FrontmatterDocumentWithUpdated returns a copy of FrontmatterDocument with Updated set to the given value.
#FrontmatterDocument.WithNavGroup
func (x *FrontmatterDocument) WithNavGroup(v string) *FrontmatterDocumentWithNavGroup returns a copy of FrontmatterDocument with NavGroup set to the given value.
#FrontmatterDocument.WithNavOrder
func (x *FrontmatterDocument) WithNavOrder(v int64) *FrontmatterDocumentWithNavOrder returns a copy of FrontmatterDocument with NavOrder set to the given value.
#FrontmatterDocument.WithType
func (x *FrontmatterDocument) WithType(v string) *FrontmatterDocumentWithType returns a copy of FrontmatterDocument with Type set to the given value.
#FrontmatterDocument.WithVersioned
func (x *FrontmatterDocument) WithVersioned(v bool) *FrontmatterDocumentWithVersioned returns a copy of FrontmatterDocument with Versioned set to the given value.
#FrontmatterDocument.WithFeed
func (x *FrontmatterDocument) WithFeed(v bool) *FrontmatterDocumentWithFeed returns a copy of FrontmatterDocument with Feed set to the given value.
#FrontmatterDocument.WithSchema
func (x *FrontmatterDocument) WithSchema(v string) *FrontmatterDocumentWithSchema returns a copy of FrontmatterDocument with Schema set to the given value.
#FrontmatterDocument.WithAutoSteps
func (x *FrontmatterDocument) WithAutoSteps(v bool) *FrontmatterDocumentWithAutoSteps returns a copy of FrontmatterDocument with AutoSteps set to the given value.
#FrontmatterDocument.WithAutoApi
func (x *FrontmatterDocument) WithAutoApi(v bool) *FrontmatterDocumentWithAutoApi returns a copy of FrontmatterDocument with AutoApi set to the given value.
#FrontmatterDocument.WithGlossaryLinks
func (x *FrontmatterDocument) WithGlossaryLinks(v bool) *FrontmatterDocumentWithGlossaryLinks returns a copy of FrontmatterDocument with GlossaryLinks set to the given value.
#FrontmatterDocument.WithLocale
func (x *FrontmatterDocument) WithLocale(v string) *FrontmatterDocumentWithLocale returns a copy of FrontmatterDocument with Locale set to the given value.
#FrontmatterDocument.WithGenerated
func (x *FrontmatterDocument) WithGenerated(v bool) *FrontmatterDocumentWithGenerated returns a copy of FrontmatterDocument with Generated set to the given value.
#FrontmatterDocument.WithSeeded
func (x *FrontmatterDocument) WithSeeded(v bool) *FrontmatterDocumentWithSeeded returns a copy of FrontmatterDocument with Seeded set to the given value.
#FrontmatterDocument.WithDraft
func (x *FrontmatterDocument) WithDraft(v bool) *FrontmatterDocumentWithDraft returns a copy of FrontmatterDocument with Draft set to the given value.
#FrontmatterDocument.WithDirectives
func (x *FrontmatterDocument) WithDirectives(v bool) *FrontmatterDocumentWithDirectives returns a copy of FrontmatterDocument with Directives set to the given value.
#FrontmatterDocument.WithVersion
func (x *FrontmatterDocument) WithVersion(v string) *FrontmatterDocumentWithVersion returns a copy of FrontmatterDocument with Version set to the given value.
#FrontmatterDocument.WithPrevVersion
func (x *FrontmatterDocument) WithPrevVersion(v string) *FrontmatterDocumentWithPrevVersion returns a copy of FrontmatterDocument with PrevVersion set to the given value.
#FrontmatterDocument.WithBumpType
func (x *FrontmatterDocument) WithBumpType(v string) *FrontmatterDocumentWithBumpType returns a copy of FrontmatterDocument with BumpType set to the given value.
#FrontmatterDocument.WithReleaseUrl
func (x *FrontmatterDocument) WithReleaseUrl(v string) *FrontmatterDocumentWithReleaseUrl returns a copy of FrontmatterDocument with ReleaseUrl set to the given value.
#FrontmatterDocument.WithRegistryUrls
func (x *FrontmatterDocument) WithRegistryUrls(v []string) *FrontmatterDocumentWithRegistryUrls returns a copy of FrontmatterDocument with RegistryUrls set to the given value.