document

package
v0.2.5 Latest Latest
Warning

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

Go to latest
Published: Jan 21, 2024 License: MIT Imports: 44 Imported by: 0

Documentation

Overview

Package document supplies a static website generator. It has these goals:

  1. New content must be easy to edit. Old content must be hard to break.
  2. Be replaceable. Special syntax should look fine as plaintext if moved elsewhere.

Winter is part of the TadiWeb.

Easy to edit, hard to break

To accomplish its first goal, Winter employs two mechanisms.

First, content managed by Winter is either warm or cold. Warm content is welcome to be synchronized into a Winter-managed directory by external tools, such as your mobile text editor or note-taking app of choice. These external tools and their synchronization steps are not provided by Winter, and they are not required to use it, but Winter's design goals assume they exist. Warm content is content you are actively working on. It is not ready to be listed anywhere, but it is published to a page on your website so you can view it in context and share it with friends for review.

Cold content is finished content. Cool URIs don't change. When your warm content is ready to be published, you should freeze it:

winter freeze src/cold/my_post.md

This converts it into cold content. The most important difference between the two is that your synchronization tools never, ever touch cold content. Sychronization tools, jobs, and scripts are run frequently and probably aren't your most robust pipelines. To limit the blast radius of any bugs they experience, content that you don't expect to edit often must be moved away from the directories they control. This is cold content.

Cold content is also guaranteed by Winter's quality checks. When a piece of content becomes cold, Winter commits its URL to memory and ensures that URL stays accessible in future runs. If ever a URL that once was accessible becomes inaccessible, Winter alerts you and prevents that build from succeeding. This protects cold content from you, your tools, and from Winter's current and future versions.

No lock-in

To accomplish its second goal, Winter provides cherries on top of Markdown. However, using the same Markdown with any other static website generator results in no degradation. Just like Markdown itself, Winter Markdown looks great when not parsed.

Galleries

A block containing only images is treated as a gallery, with the images placed in a responsive grid. Images can be clicked to zoom in or out.

![An image of a cat.](/img/cat.jpg)
![An image of a dog.](/img/dog.jpg)

Image captions

A block of all-italic text immediately below an image or gallery is treated as a caption, and given a special visual treatment and accessibility structure.

![An image of a cat.](/img/cat.jpg)
![An image of a dog.](/img/dog.jpg)

_My cat and dog like to play with each other._

Photos as first-class citizens

For photographers, Winter supports photographs as first-class citizens. EXIF data is automatically extracted and can be displayed using template variables. Photos organized into galleries display next to each other neatly, with a built-in lightbox.

Photo EXIF safety

Any photos Winter processes that have GPS data embedded in them are loudly rejected, failing the build.

Reduced load times

Images and references to them are automatically converted into WebP format and several thumbnails are generated for each. Each image renders with <img srcset> to ensure only the smallest possible image that saturates the display density is loaded.

LaTeX

Surround text in $dollar signs$ to render LaTeX, implemented via KaTeX.

$\LaTeX$ users rejoice!

Tables of contents

A table of contents can be requested by setting the toc variable to true in frontmatter. Tables of contents are always rendered immediately above the first level-2 heading.

---
toc: true
---

# My Article

(table of contents will be rendered here)

## My Cat

...

## My Dog

...

Syntax highlighting

Fenced code blocks that specify a language are syntax-highlighted.

```go
package main
func main() {
  fmt.Println("I'm syntax highlighted!")
}
```

Any links that navigate to external websites will automatically have a target=_blank set during generation.

Dark mode images

If an image's extensionless filename ends in "-dark" or "-light", and another image exists at the same path but with the opposite suffix, the correct one will be rendered to the user based on their light-/dark-mode preference.

Index

Constants

View Source
const (
	AppName = "winter"
)

Variables

This section is empty.

Functions

func ConfigPath

func ConfigPath() (string, error)

func InteractiveConfig

func InteractiveConfig() error

func NewIMG

func NewIMG(logger *slog.Logger, src string, cfg *Config) (*img, error)

NewIMG returns a struct that represents an image to be built. The returned value implements Document.

Types

type Config

type Config struct {
	// Author is the information for the website author.
	// This is used in metadata such as that of the RSS feed.
	Author feeds.Author `yaml:"author,omitempty"`
	// Debug is a flag that enables debug mode.
	Debug bool `yaml:"debug,omitempty"`
	// Development contains options specific to development.
	// They have no impact when building for production.
	Development struct {
		// URL is the base URL you will connect to while developing your website or Winter.
		// If blank, it defaults to "http://localhost:8100".
		URL string `yaml:"url,omitempty"`
	} `yaml:"development,omitempty"`
	// Description is the Description of the website.
	// This is used as metadata for the RSS feed.
	Description string `yaml:"description,omitempty"`
	// Dist is the location the site will be built into,
	// relative to the working directory.
	// After a build, this directory is suitable for deployment to the web as a set of static files.
	//
	// In other words, the path of any file in dist,
	// relative to dist,
	// is equivalent to the path component of the URL for that file.
	//
	// If blank, defaults to ./dist.
	Dist string `yaml:"dist,omitempty"`
	// Known helps the generated site follow the "Cool URIs don't change" rule
	// by remembering certain facts about what the site looks like,
	// and checking newly-generated sites against those facts.
	Known struct {
		// URIs holds the path to the known URIs file,
		// which Winter will generate, update, and maintain.
		//
		// You should commit this file.
		//
		// If unset, defaults to src/uris.txt.
		URIs string `yaml:"urls,omitempty"`
	} `yaml:"known,omitempty"`
	// Name is the name of the website.
	// This is used in various places in and out of templates.
	Name string `yaml:"name,omitempty"`
	// Gear is an array of Gear objects,
	// each describing a camera, lens, or other piece of gear
	// whose information can be extracted from EXIF data.
	//
	// Gear is used by Winter when processing photos to display photograph information,
	// and provide links to purchase gear used in its creation.
	Gear       []Gear `yaml:"gear,omitempty"`
	Production struct {
		// URL is the base URL you will connect to to view your deployed website
		// (e.g. twos.dev or one.twos.dev or twos.dev:6667).
		// This is used in various backlinks, like those in the RSS feed.
		//
		// Must not be blank.
		URL string `yaml:"url,omitempty"`
	} `yaml:"production,omitempty"`
	// Since is the year the website was established,
	// whether through Winter or otherwise.
	// This is used as metadata for the RSS feed,
	// and as a copyright notice when needed.
	Since int `yaml:"since,omitempty"`
	// Src is an additional list of directories to search for source files beyond ./src.
	Src []string `yaml:"srca,omitempty"`
}

Config is a configuration for the Winter build.

func NewConfig

func NewConfig() (*Config, error)

func (*Config) GearByString added in v0.2.0

func (c *Config) GearByString(make, model string) (g *Gear, ok bool)

GearByString returns the Gear item for the given make and model EXIF tag values. If none could be found, ok is false.

func (*Config) Save

func (c *Config) Save() error

type Document

type Document interface {
	// DependsOn returns true if and only if the given source path,
	// when changed,
	// should cause this document to be rebuilt.
	DependsOn(src string) bool
	// Load reads or re-reads the source file from disk,
	// overwriting any previously stored or parsed contents.
	Load(r io.Reader) error
	// Metadata returns data about the document,
	// which may have been inferred automatically or set by frontmatter.
	Metadata() *Metadata
	// Render generates the final HTML for the document and writes it to w.
	Render(w io.Writer) error
}

Document is something that can be built, usually from a source file on disk to a destination file on disk.

After a document has been built by calling [Build], it can be passed to a template during execution:

var buf bytes.Buffer
t.Execute(&buf, d)

type EXIF

type EXIF struct {
	Aperture    float64
	Camera      *Gear
	FocalLength float64
	ISO         string
	// Lens holds information about the lens used for the photo.
	// If the photo EXIF data has no or insufficient lens information,
	// Lens is nil.
	Lens         *Gear
	ShutterSpeed string
	TakenAt      time.Time
}

type ErrNotTracked

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

func (ErrNotTracked) Error

func (err ErrNotTracked) Error() string

type Gear

type Gear struct {
	// Make is the user-readable brand that created this piece of gear.
	Make string `yaml:"make,omitempty"`
	// Model is the user-readable model for this piece of gear.
	Model string `yaml:"model,omitempty"`
	// Link is a URL
	// (beginning in https://)
	// at which a user can purchase or read more about this piece of gear.
	Link string `yaml:"link,omitempty"`
	// EXIF specifies what EXIF data a photograph must have in order to be identified as having been taken with this piece of gear.
	EXIF struct {
		// Make is the value a photograph's EXIF data must have in the "make" field,
		// whether camera or lens,
		// for the photograph to be considered as having been taken with this piece of gear.
		//
		// Before comparison,
		// Winter will strip all trailing and leading spaces from both the EXIF data field and from this field.
		// Then, a case-insensitive comparison is performed.
		Make string `yaml:"make,omitempty"`
		// Make is the value a photograph's EXIF data must have in the "model" field,
		// whether camera or lens,
		// for the photograph to be considered as having been taken with this piece of gear.
		//
		// Before comparison,
		// Winter will strip all trailing and leading spaces from both the EXIF data field and from this field.
		// Then, a case-insensitive comparison is performed.
		Model string `yaml:"model,omitempty"`
	} `yaml:"exif,omitempty"`
}

type GeminiDocument added in v0.2.3

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

GeminiDocument represents a document written in or converted to gemtext, the markup language for Geminispace (https://geminiprotocol.net/docs/gemtext.gmi).

GeminiDocument implements Document.

func NewGeminiDocument added in v0.2.3

func NewGeminiDocument(src string, meta *Metadata) *GeminiDocument

NewGeminiDocument creates a new gemtext document whose original source is at path src.

Nothing is read from disk; src is metadata. To read and parse gemtext, call [Load].

func (*GeminiDocument) DependsOn added in v0.2.3

func (doc *GeminiDocument) DependsOn(src string) bool

func (*GeminiDocument) Load added in v0.2.3

func (doc *GeminiDocument) Load(r io.Reader) error

Load reads Gemini from r and loads it into doc.

If called more than once, the last call wins.

func (*GeminiDocument) Metadata added in v0.2.3

func (doc *GeminiDocument) Metadata() *Metadata

func (*GeminiDocument) Render added in v0.2.3

func (doc *GeminiDocument) Render(w io.Writer) error

type GeminiRenderer added in v0.2.3

type GeminiRenderer interface {
	// RenderGemini converts the document into gemtext,
	// then writes the result to w.
	RenderGemini(w io.Writer) error
}

GeminiRenderer is a type that can render itself into gemtext, the markup language for Geminispace, an alternate web.

type HTMLDocument

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

HTMLDocument represents the HTML for a source file.

The document's source content may or may not be in HTML. It may be that the source file was written in another language, like Markdown, then converted to HTML via MarkdownDocument.

Therefore, any page generated from a file in src will at some point be an HTMLDocument.

HTMLDocument implements Document.

func NewHTMLDocument

func NewHTMLDocument(src string, meta *Metadata, next Document) *HTMLDocument

NewHTMLDocument creates a new document whose original source is at path src.

Nothing is read from disk; src is metadata. It may or may not point to a file containing HTML. To read and parse HTML, call [Load].

func (*HTMLDocument) DependsOn

func (doc *HTMLDocument) DependsOn(src string) bool

func (*HTMLDocument) Load

func (doc *HTMLDocument) Load(r io.Reader) error

Load reads HTML from r and loads it into doc.

If called more than once, the last call wins.

func (*HTMLDocument) Massage

func (doc *HTMLDocument) Massage() error

Massage messes with loaded content to improve the page when it is ultimately rendered.

Massage performs these tasks:

  • Linkifies and stylizes the first <h1> as a page title
  • Generates a table of contents, if requested by metadata
  • Sets target=_blank for all <a> tags pointing to external sites
  • Generates a preview for the document, if one wasn't manually specified
  • Syntax-highlights code blocks

func (*HTMLDocument) Metadata

func (doc *HTMLDocument) Metadata() *Metadata

func (*HTMLDocument) Post

func (doc *HTMLDocument) Post() bool

func (*HTMLDocument) Render

func (doc *HTMLDocument) Render(w io.Writer) error

Render encodes any loaded content into HTML and writes it to w.

type LayoutDocument

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

LayoutDocument represents a document to be assembled by being placed inside a layout-style template.

For example, a blog post document would not itself contain a site header or footer; instead, it would be fully rendered then placed inside a layout which includes it:

{{ template "body" }}

The LayoutDocument's job is to facilitate that embedding. It will usually come last in the load/render chain.

func NewLayoutDocument

func NewLayoutDocument(src string, meta *Metadata, next Document) *LayoutDocument

func (*LayoutDocument) DependsOn

func (doc *LayoutDocument) DependsOn(src string) bool

func (*LayoutDocument) Load

func (doc *LayoutDocument) Load(r io.Reader) error

func (*LayoutDocument) Metadata

func (doc *LayoutDocument) Metadata() *Metadata

func (*LayoutDocument) Render

func (doc *LayoutDocument) Render(w io.Writer) error

type MarkdownDocument

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

MarkdownDocument represents a source file written in Markdown, with optional Go template syntax embedded in it.

MarkdownDocument implements Document.

The MarkdownDocument is transitory; its only purpose is to create a TemplateDocument.

func NewMarkdownDocument

func NewMarkdownDocument(src string, meta *Metadata, next map[Document]struct{}) *MarkdownDocument

NewMarkdownDocument creates a new document whose original source is at path src.

Nothing is read from disk; src is metadata. To read and parse Markdown, call [Load].

func (*MarkdownDocument) DependsOn

func (doc *MarkdownDocument) DependsOn(src string) bool

func (*MarkdownDocument) Load

func (doc *MarkdownDocument) Load(r io.Reader) error

Load reads Markdown from r and loads it into doc.

If called more than once, the last call wins.

func (*MarkdownDocument) Metadata

func (doc *MarkdownDocument) Metadata() *Metadata

func (*MarkdownDocument) Render

func (doc *MarkdownDocument) Render(w io.Writer) error

func (*MarkdownDocument) RenderGemini added in v0.2.3

func (doc *MarkdownDocument) RenderGemini(w io.Writer) error

type Metadata

type Metadata struct {
	// Category is an optional category for the document. This is used
	// only for a small visual treatment on the index page (if this is
	// of kind post) and on the document page itself.
	//
	// Category MUST be a singular noun that can be pluralized by adding
	// a single "s" at its end, as this is exactly what the visual
	// treatment will do. If this doesn't work for you, go fix that
	// code.
	Category string `yaml:"category,omitempty"`
	// CreatedAt is the time the document was first published.
	CreatedAt time.Time `yaml:"date,omitempty"`
	// Kind specifies the type of document this is.
	// In every user-facing context, this is called "type".
	// In Go we cannot use the "type" keyword, so we use "kind" instead.
	Kind kind `yaml:"type,omitempty"`
	// Layout is the path to the source file for the layout this document should be rendered into.
	//
	// If unset, src/templates/text_document.html.tmpl is used.
	Layout string `yaml:"layout,omitempty"`
	// GeminiPath is the path component of the Geminispace URL that will point to this document,
	// once rendered.
	// GeminiPath MUST NOT contain any slashes;
	// everything is top-level.
	//
	// GeminiPath is equivalent to the path to the destination file relative to dist.
	GeminiPath string `yaml:"-,omitempty"`
	// ParentFilename is the filename component of another document that this one is a child of.
	// Parenthood is a purely semantic relationship for the benefit of the user.
	// Templates can access parents to influence rendering.
	ParentFilename string `yaml:"parent,omitempty"`
	// Preview is a sentence-long blurb of the document,
	// to be shown along with its title as a teaser of its contents.
	Preview string `yaml:"preview,omitempty"`
	// SourcePath is the location on disk of the original file that this document represents.
	// It is relative to the working directory.
	SourcePath string `yaml:"-"`
	// TemplateDir is the location on disk of a directory containing any templates that will be used in the document.
	// By default, it is src/templates.
	TemplateDir string `yaml:"-"`
	// Title is the human-readable title of the document.
	Title string `yaml:"title,omitempty"`
	// TOC is whether a table of contents should be rendered with the
	// document. If true, the table of contents is rendered immediately
	// above the first non-first-level heading.
	TOC bool `yaml:"toc,omitempty"`
	// UpdatedAt is the time the document was last meaningfully updated.
	UpdatedAt time.Time `yaml:"updated,omitempty"`
	// WebPath is the path component of the URL that will point to this document,
	// once rendered.
	// WebPath MUST NOT contain any slashes;
	// everything is top-level.
	//
	// WebPath is equivalent to the path to the destination file
	// relative to dist.
	WebPath string `yaml:"filename,omitempty"`
}

Metadata holds information about a Document that isn't inside the document itself.

func NewMetadata

func NewMetadata(src, tmplDir string) *Metadata

NewMetadata returns a Metadata with some defaults filled in according path src.

NewMetadata is purely lexicographic; no files are opened or read.

Defaults that depend on parsing the content of the document, such as a Preview generated from its content, are not filled in.

func (*Metadata) IsType

func (meta *Metadata) IsType(t string) bool

func (*Metadata) UnmarshalDocument added in v0.2.2

func (meta *Metadata) UnmarshalDocument(r io.Reader) ([]byte, error)

UnmarshalDocument parses the metadata from the given reader, then reads and returns the remaining bytes.

type OrgDocument

type OrgDocument struct {
	// SourcePath is the path on disk to the file this Org is read from or generated from.
	// The path is relative to the working directory.
	SourcePath string
	// contains filtered or unexported fields
}

OrgDocument represents a source file written in Org, with optional Go template syntax embedded in it.

OrgDocument implements Document.

The OrgDocument is transitory; its only purpose is to create an HTMLDocument.

func NewOrgDocument

func NewOrgDocument(src string, meta *Metadata, next Document) *OrgDocument

NewOrgDocument creates a new document whose original source is at path src.

Nothing is read from disk; src is metadata. To read and parse Org, call [Load].

func (*OrgDocument) DependsOn

func (doc *OrgDocument) DependsOn(src string) bool

func (*OrgDocument) Load

func (d *OrgDocument) Load(r io.Reader) error

Load reads Org from r and loads it into doc.

If called more than once, the last call wins.

func (*OrgDocument) Metadata

func (doc *OrgDocument) Metadata() *Metadata

func (*OrgDocument) Render

func (doc *OrgDocument) Render(w io.Writer) error

type StaticDocument

type StaticDocument struct {
	SourcePath string
	// contains filtered or unexported fields
}

StaticDocument represents a file on disk that will be copied as-is to the web root. The subdirectory of the file relative to the web root will match the relative directory of the source file relative to the ./public directory.

func NewStaticDocument

func NewStaticDocument(src, webPath string) *StaticDocument

NewStaticDocument creates a new document whose original source is at path src, and whose desired web path is webPath.

Nothing is read from disk; src is metadata. To read the static file, call [Load].

func (*StaticDocument) DependsOn

func (doc *StaticDocument) DependsOn(src string) bool

func (*StaticDocument) Load

func (doc *StaticDocument) Load(r io.Reader) error

func (*StaticDocument) Metadata

func (doc *StaticDocument) Metadata() *Metadata

func (*StaticDocument) Render

func (doc *StaticDocument) Render(w io.Writer) error

type Substructure

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

Substructure is a graph of documents on the website.

func NewSubstructure

func NewSubstructure(logger *slog.Logger, cfg *Config) (*Substructure, error)

NewSubstructure returns a substructure with the given configuration. Upon initialization, a substructure is the result of a discovery phase of content on the filesystem. Further calls are needed to build the full graph of content and render it to HTML.

func (*Substructure) Build

func (s *Substructure) Build(doc Document) error

Build builds doc.

To also build downstream dependencies, use [Rebuild] instead.

func (*Substructure) DocBySourcePath

func (s *Substructure) DocBySourcePath(path string) (doc Document, ok bool)

DocBySourcePath returns the document that originated from the file at path.

If no such document exists, ok is false.

func (*Substructure) ExecuteAll

func (s *Substructure) ExecuteAll(dist string) error

ExecuteAll builds all documents known to the substructure, as well as any site-scoped non-documents such as RSS feeds.

func (*Substructure) Rebuild

func (s *Substructure) Rebuild(src string) error

Rebuild rebuilds the document or template at the given path. Then, it rebuilds any downstream dependencies.

If src isn't known to the substructure, Rebuild no-ops and returns no error.

func (*Substructure) SaveNewURIs

func (s *Substructure) SaveNewURIs(dist string) error

SaveNewURIs indexes every HTML file in dist and saves their existence to disk. Later, validateURIsDidNotChange can read that file and ensure no file is missing.

The database file's location can be customized with winter.yml. It should be commited to the repository.

type TemplateDocument

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

TemplateDocument represents a source file containing Go template clauses. The surrounding syntax can be anything.

Note that TemplateDocument does not handle layout-style templates, where the document itself should be embedded in another template. For that behavior, use LayoutDocument further down the load/render chain.

TemplateDocument implements Document.

The TemplateDocument is transitory; its only purpose is to resolve templates then hand off the resolved source to another Document type.

func NewTemplateDocument

func NewTemplateDocument(src string, meta *Metadata, docs *documents, photos map[string][]*img, next Document) *TemplateDocument

NewTemplateDocument returns a template document with the given pointers to existing document metadata, substructure docs, and substructure photos.

func (*TemplateDocument) DependsOn

func (doc *TemplateDocument) DependsOn(src string) bool

func (*TemplateDocument) Load

func (doc *TemplateDocument) Load(r io.Reader) error

Load reads a Go template from r and loads it into doc. Any templates referenced within are looked for looked for by name, relative to the working directory.

To use a template, treat its filepath as a name:

{{ template "_foo.html.tmpl" }}

Any referenced templates will be loaded as well, and attached to the main template. This operation is recursive.

If called more than once, the last call wins.

func (*TemplateDocument) Metadata

func (doc *TemplateDocument) Metadata() *Metadata

func (*TemplateDocument) Render

func (doc *TemplateDocument) Render(w io.Writer) error

type TemplateNode

type TemplateNode struct {
	ast.Leaf
	Raw []byte
}

Jump to

Keyboard shortcuts

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