content

package
v1.0.0-beta.20 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Index

Constants

View Source
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

View Source
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(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

func CalculateReadingTime(wordCount, imageCount int) int

CalculateReadingTime returns estimated reading time in minutes Formula: ceil((words / 200) + (images × 0.2)) Minimum return value is 1 minute

func CheckSymlinkEscape

func CheckSymlinkEscape(root, path string, mode os.FileMode) error

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

func CountImages(htmlContent string) int

CountImages counts the number of <img> tags in HTML content

func CountWords

func CountWords(htmlContent string) int

CountWords counts words in HTML content by stripping tags first

func ExtractInlineTags

func ExtractInlineTags(markdown string) []string

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

func MergeTags(explicit, inline []string) []string

MergeTags combines explicit metadata and inline tags, keeping explicit tag order and spelling authoritative while removing case-variant duplicates.

func NormalizeTags

func NormalizeTags(tags []string) []string

NormalizeTags preserves the first spelling of each tag while removing case-variant duplicates from a single page.

func ParseDate

func ParseDate(dateStr string) (time.Time, error)

ParseDate parses the date string from frontmatter

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

func UsesMermaid(pages []*Page) bool

UsesMermaid reports whether any page's rendered HTML contains a Mermaid diagram. Call only after markdown has been rendered into HTMLContent.

func ValidateOutputRoutes

func ValidateOutputRoutes(pages []*Page) error

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

func ParseSingleFile(rootDir, relPath string) (*Page, error)

ParseSingleFile parses a single markdown file and returns a Page

func (*Page) DisplayDate

func (p *Page) DisplayDate() string

DisplayDate returns the most relevant date (modified if exists, otherwise created)

func (*Page) DisplayDateISO

func (p *Page) DisplayDateISO() string

DisplayDateISO returns the most relevant date in ISO format

func (*Page) FormattedDate

func (p *Page) FormattedDate() string

FormattedDate returns the date in a human-readable format

func (*Page) FormattedModified

func (p *Page) FormattedModified() string

FormattedModified returns the modified date in a human-readable format

func (*Page) GrowthEmoji

func (p *Page) GrowthEmoji() string

GrowthEmoji returns the emoji for the growth stage

func (*Page) HasModified

func (p *Page) HasModified() bool

HasModified returns true if the page has a modified date different from created

func (*Page) ISODate

func (p *Page) ISODate() string

ISODate returns the date in ISO format for datetime attribute

func (*Page) ISOModified

func (p *Page) ISOModified() string

ISOModified returns the modified date in ISO format

func (*Page) PlainContent

func (p *Page) PlainContent() string

PlainContent returns content with HTML tags stripped for search indexing

func (*Page) ReadingTimeDisplay

func (p *Page) ReadingTimeDisplay() string

ReadingTimeDisplay returns a human-readable reading time string

func (*Page) SEODescription

func (p *Page) SEODescription() string

SEODescription returns the description for meta tags Uses frontmatter description if set, otherwise auto-generates from content

func (*Page) ShortDate

func (p *Page) ShortDate() string

ShortDate returns the date in short format

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) Render

func (r *Renderer) Render(content string) (string, []string)

Render converts markdown to HTML, processing wiki-links

func (*Renderer) SetEscapeRawHTML

func (r *Renderer) SetEscapeRawHTML(escape bool)

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 &lt;script&gt; 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 (r *Renderer) SetPlainBrokenLinks(plain bool)

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

type ResolveResult struct {
	Page      *Page
	Ambiguous bool
	Broken    bool
}

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

func NewScanner(rootDir string, ignore []string) *Scanner

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.

func (*Scanner) Scan

func (s *Scanner) Scan() ([]*Page, error)

Scan walks the directory tree and returns all markdown files

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(content string) []WikiLink

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.

Jump to

Keyboard shortcuts

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