Documentation
¶
Index ¶
- Constants
- Variables
- func BuildBacklinks(pages []*Page, resolver ...*LinkResolver)
- func CalculateReadingTime(wordCount, imageCount int) int
- func CheckSymlinkEscape(root, path string, mode os.FileMode) error
- func CountImages(htmlContent string) int
- func CountWords(htmlContent string) int
- func ExtractInlineTags(markdown string) []string
- func IsExcluded(relPath string, ignore *IgnoreMatcher) bool
- func MergeTags(explicit, inline []string) []string
- func NormalizeTags(tags []string) []string
- func ParseDate(dateStr string) (time.Time, error)
- func PopulateOutLinks(pages []*Page)
- func RenderPages(pages []*Page, enableWikilinks bool, resolver *LinkResolver, basePath string) []string
- func UsesMermaid(pages []*Page) bool
- func ValidateOutputRoutes(pages []*Page) error
- type Frontmatter
- type IgnoreMatcher
- type LinkResolver
- type Page
- func (p *Page) DisplayDate() string
- func (p *Page) DisplayDateISO() string
- func (p *Page) FormattedDate() string
- func (p *Page) FormattedModified() string
- func (p *Page) GrowthEmoji() string
- func (p *Page) HasModified() bool
- func (p *Page) ISODate() string
- func (p *Page) ISOModified() string
- func (p *Page) PlainContent() string
- func (p *Page) ReadingTimeDisplay() string
- func (p *Page) SEODescription() string
- func (p *Page) ShortDate() string
- type Renderer
- type ResolveResult
- type Scanner
- type WikiLink
Constants ¶
const ( // WordsPerMinute is the reading speed for dense/technical content // Lower than casual reading (200-250) to account for re-reading, // processing technical concepts, and following wiki-links WordsPerMinute = 150 // SecondsPerImage is time spent viewing informative images (12 seconds) SecondsPerImage = 12 )
Variables ¶
var ReservedPaths = map[string]bool{ "leafpress.json": true, "style.css": true, "static": true, "_site": true, ".leafpress": true, ".git": true, ".gitignore": true, ".obsidian": true, "node_modules": true, }
ReservedPaths are top-level names the content scan never treats as content, because Leafpress or the surrounding tooling owns them. Anything else is content; authors exclude their own folders with build.ignore.
Functions ¶
func BuildBacklinks ¶
func BuildBacklinks(pages []*Page, resolver ...*LinkResolver)
BuildBacklinks populates the Backlinks and OutLinks fields on all pages. If resolver is nil, a new one will be created.
func CalculateReadingTime ¶
CalculateReadingTime returns estimated reading time in minutes Formula: ceil((words / 200) + (images × 0.2)) Minimum return value is 1 minute
func CheckSymlinkEscape ¶
CheckSymlinkEscape reports an error when path is a symlink that resolves outside root.
A garden is a publishing boundary: everything under it is intended to be world-readable, and everything outside it is not. A link such as notes/leak.md -> ~/.ssh/id_rsa is read and published like any other note, so the escape is refused rather than silently followed. Links that stay inside the garden keep working — they resolve to content that was already going to be published.
path must be the leaf being considered. Both tree walkers use Lstat semantics and therefore never descend through a directory symlink, so the leaf is the only place a link can be introduced.
func CountImages ¶
CountImages counts the number of <img> tags in HTML content
func CountWords ¶
CountWords counts words in HTML content by stripping tags first
func ExtractInlineTags ¶
ExtractInlineTags returns unique inline tags in source order. Goldmark's parser keeps code spans, fenced code, raw HTML tags, link destinations, and escaped hashes out of the InlineTag node stream.
func IsExcluded ¶
func IsExcluded(relPath string, ignore *IgnoreMatcher) bool
IsExcluded reports whether a path relative to the garden root is outside the content set: a reserved top-level name, a hidden entry, or an ignore-glob match. Directory matches prune the whole subtree.
The content scan and the serve watcher share this predicate. When they disagree, `leafpress serve` publishes pages that `leafpress build` drops.
func MergeTags ¶
MergeTags combines explicit metadata and inline tags, keeping explicit tag order and spelling authoritative while removing case-variant duplicates.
func NormalizeTags ¶
NormalizeTags preserves the first spelling of each tag while removing case-variant duplicates from a single page.
func PopulateOutLinks ¶
func PopulateOutLinks(pages []*Page)
PopulateOutLinks extracts each page's wiki-link targets independently of whether backlinks are enabled. Duplicate targets are retained here because distinct aliases may resolve differently; artifact builders deduplicate after resolution.
func RenderPages ¶
func RenderPages(pages []*Page, enableWikilinks bool, resolver *LinkResolver, basePath string) []string
RenderPages renders HTML content for all pages in parallel If resolver is nil, a new one will be created
func UsesMermaid ¶
UsesMermaid reports whether any page's rendered HTML contains a Mermaid diagram. Call only after markdown has been rendered into HTMLContent.
func ValidateOutputRoutes ¶
ValidateOutputRoutes rejects page sets whose generated HTML would claim the same URL more than once. Besides duplicate page slugs, this accounts for section indexes synthesized for directories and tag pages synthesized from metadata.
Types ¶
type Frontmatter ¶
type Frontmatter struct {
Title string `yaml:"title"`
Description string `yaml:"description"` // SEO meta description
Date string `yaml:"date"`
Tags []string `yaml:"tags"`
Draft bool `yaml:"draft"`
Growth string `yaml:"growth"`
Sort string `yaml:"sort"` // For _index.md files
TOC *bool `yaml:"toc"` // Override site-wide TOC setting (nil = use site default)
ShowList *bool `yaml:"showList"` // Show page list on section index (nil = true)
Image string `yaml:"image"` // OG image override for this page
// Obsidian-compatible date aliases
Created string `yaml:"created"` // Alias for date (creation date)
CreatedAt string `yaml:"createdAt"` // Alias for date (creation date)
Modified string `yaml:"modified"` // Last modified date
Updated string `yaml:"updated"` // Alias for modified
UpdatedAt string `yaml:"updatedAt"` // Alias for modified
// Reading time override
ReadingTime *int `yaml:"readingTime"` // Manual override for reading time in minutes
}
Frontmatter represents the YAML frontmatter of a page
func ParseFrontmatter ¶
func ParseFrontmatter(content string) (*Frontmatter, string, error)
ParseFrontmatter extracts frontmatter and content from markdown
func (*Frontmatter) GetCreatedDate ¶
func (fm *Frontmatter) GetCreatedDate() string
GetCreatedDate returns the creation date with priority: date > created > createdAt
func (*Frontmatter) GetModifiedDate ¶
func (fm *Frontmatter) GetModifiedDate() string
GetModifiedDate returns the modified date with priority: modified > updated > updatedAt
type IgnoreMatcher ¶
type IgnoreMatcher struct {
// contains filtered or unexported fields
}
IgnoreMatcher applies the glob patterns from build.ignore.
The documented shapes are gitignore-flavoured:
drafts a name with no slash matches at any depth drafts/** everything under drafts/, and drafts/ itself *.draft.md matches the file name at any depth private/** as above notes/*.wip.md a pattern with a slash is anchored at the garden root
Within a path segment the usual path.Match metacharacters apply (* ? [x-z]); ** additionally spans separators.
func NewIgnoreMatcher ¶
func NewIgnoreMatcher(patterns []string) (*IgnoreMatcher, error)
NewIgnoreMatcher compiles patterns, rejecting malformed ones so a typo surfaces as a config error instead of silently ignoring nothing.
func (*IgnoreMatcher) Match ¶
func (m *IgnoreMatcher) Match(relPath string) bool
Match reports whether relPath is ignored. relPath is relative to the garden root, in OS form. Callers walking a tree should prune on a directory match: because ** also matches zero segments, "drafts/**" matches the drafts directory itself, not only its contents.
type LinkResolver ¶
type LinkResolver struct {
// contains filtered or unexported fields
}
LinkResolver resolves wiki-links to actual pages
func NewLinkResolver ¶
func NewLinkResolver(pages []*Page) *LinkResolver
NewLinkResolver creates a new link resolver
func (*LinkResolver) AddAlias ¶
func (r *LinkResolver) AddAlias(name string, page *Page)
AddAlias registers an additional name (e.g. a page's display title) that resolves to [page]. Matching is case- and interior-whitespace-insensitive. Slug and filename matches take precedence. When two pages claim the same alias, the lexicographically smaller slug wins — like nameMap's ambiguous resolution — so the outcome does not depend on registration order (CLI scan order vs renderer input order).
func (*LinkResolver) Resolve ¶
func (r *LinkResolver) Resolve(target string) ResolveResult
Resolve resolves a wiki-link target to a page
type Page ¶
type Page struct {
// Metadata from frontmatter
Title string
Description string // SEO meta description (from frontmatter or auto-generated)
Date time.Time // Primary display date (from date, created, or createdAt)
Created time.Time // Creation date (from created, createdAt, or date)
Modified time.Time // Last modified date (from modified, updated, or updatedAt)
Tags []string
Draft bool
Growth string // seedling | budding | evergreen
TOC *bool // Override site-wide TOC setting (nil = use site default)
ShowList *bool // Show page list on section index (nil = true)
Image string // OG image for this page (from frontmatter)
// Paths
SourcePath string // Relative path to .md file (e.g., "projects/leafpress.md")
Slug string // URL slug (e.g., "projects/leafpress")
OutputPath string // Path in _site/ (e.g., "projects/leafpress/index.html")
Permalink string // Full URL path (e.g., "/projects/leafpress/")
// Content
RawContent string // Original markdown (without frontmatter)
HTMLContent string // Rendered HTML
// Relationships
Backlinks []*Page // Pages that link to this page
OutLinks []string // Wiki-link targets (slugs)
// Reading time
WordCount int // Total word count
ImageCount int // Number of images
ReadingTime int // Estimated reading time in minutes
ReadingTimeOverride *int // Manual override from frontmatter
// Section
IsIndex bool // Is this a section index (_index.md)?
SectionSort string // Sort order for section pages (date|title|growth)
}
Page represents a content page
func ParseSingleFile ¶
ParseSingleFile parses a single markdown file and returns a Page
func (*Page) DisplayDate ¶
DisplayDate returns the most relevant date (modified if exists, otherwise created)
func (*Page) DisplayDateISO ¶
DisplayDateISO returns the most relevant date in ISO format
func (*Page) FormattedDate ¶
FormattedDate returns the date in a human-readable format
func (*Page) FormattedModified ¶
FormattedModified returns the modified date in a human-readable format
func (*Page) GrowthEmoji ¶
GrowthEmoji returns the emoji for the growth stage
func (*Page) HasModified ¶
HasModified returns true if the page has a modified date different from created
func (*Page) ISOModified ¶
ISOModified returns the modified date in ISO format
func (*Page) PlainContent ¶
PlainContent returns content with HTML tags stripped for search indexing
func (*Page) ReadingTimeDisplay ¶
ReadingTimeDisplay returns a human-readable reading time string
func (*Page) SEODescription ¶
SEODescription returns the description for meta tags Uses frontmatter description if set, otherwise auto-generates from content
type Renderer ¶
type Renderer struct {
// contains filtered or unexported fields
}
Renderer converts markdown to HTML
func NewRenderer ¶
func NewRenderer(resolver *LinkResolver, enableWikilinks bool, basePath string) *Renderer
NewRenderer creates a new markdown renderer
func (*Renderer) SetEscapeRawHTML ¶
SetEscapeRawHTML controls how raw HTML in markdown is rendered. By default (false), raw HTML passes through unchanged (goldmark's unsafe mode) — appropriate for trusted single-author content. When enabled, raw HTML renders as visibly escaped text (e.g. <script> becomes <script> in the output, so the reader sees the literal characters the author typed), and renderer-generated HTML (wikilinks, callouts, media embeds) is still emitted live. Call this before Render; it is not safe to call concurrently with Render.
func (*Renderer) SetPlainBrokenLinks ¶
SetPlainBrokenLinks controls how unresolved wikilinks are rendered. By default (false), a broken wikilink renders as a styled span (<span class="lp-broken-link">…</span>). When enabled, broken wikilinks render as plain display text instead — no anchor, no class.
type ResolveResult ¶
ResolveResult represents the result of resolving a wiki-link
type Scanner ¶
type Scanner struct {
// contains filtered or unexported fields
}
Scanner scans the content directory for markdown files
func NewScanner ¶
NewScanner creates a new content scanner. A malformed ignore pattern is held until Scan so the constructor keeps its single-value signature; Config.Validate reports the same problem earlier and more clearly.
type WikiLink ¶
type WikiLink struct {
Target string // The link target (slug or path)
Label string // Display label (defaults to target)
Raw string // Original raw text including brackets
}
WikiLink represents a parsed wiki-link
func ExtractWikiLinks ¶
ExtractWikiLinks extracts wikilinks from Markdown syntax nodes. Code spans, fenced/indented code, escaped brackets, raw HTML attributes, and ordinary Markdown link/image labels are therefore excluded by the same parser rules used for rendering.