[READ-ONLY] Mirror of https://github.com/darccio/zas. Most simple static website generator in Golang.
go static-site-generation static-site-generator
Something went wrong. Try again.
zas data.go
9.2 kB · 243 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244/* * Copyright (c) 2013 Dario Castañé. * This file is part of Zas. * * Zas is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Zas is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Zas. If not, see <http://www.gnu.org/licenses/>. */
package zas
import ( "errors" "fmt" thtml "html/template" "path" "path/filepath" "strings"
"github.com/darccio/zas/internal/i18n")
// ZasData is the context data store used in templates.type ZasData struct { // Template used as body from current file. // // Body is only ever populated after this page's own template has // finished executing - render derives it from that very execution's // HTML5-parsed output (see render in generate.go) - so {{.Body}} // always evaluates empty when used from inside the page's own body // content, unlike in the layout template, which only runs later, once // Body is already populated (see Generate in generate.go). This is an // inherent limitation, not a bug: giving a page's own body a preview // of its own final rendered self would need a multi-pass rendering // model. Contrast with Page and FirstTitle below, which render does // give the page's own body a best-effort preview of, extracted from // the page's raw source ahead of its own template execution (see // earlyPageConfig and leadingH1Text in generate.go) precisely because // neither is circularly defined in terms of that same execution. Body thtml.HTML // Current path (usable in URLs). Path string // Title from first level header (H1). // // Inside the page's own body content (as opposed to the layout), // FirstTitle holds a best-effort preview extracted from the page's raw // source before its own template executes - see leadingH1Text in // generate.go - rather than the canonical value render later derives // from the fully rendered, HTML5-parsed document. The two normally // agree; the preview is left unset only when no leading <h1> is found, // or when one is found but its content still contains unexecuted // template syntax ("{{"), to avoid exposing placeholder text as if it // were a real title. FirstTitle string // Site configuration, as found in ConfigFile. Site ZasSiteData // In-page configuration, from first HTML comment (expected as YAML map). Page map[interface{}]interface{} // Current directory configuration, from DirConfigFile. Directory ConfigSection // Config loaded from ConfigFile. config ConfigSection // i18n helper i18n *i18n.Build // Attributes from the source page's own <body> tag (if any), merged // onto the layout's <body> element since Body only carries the source // body's inner HTML, not the element itself. bodyAttrs map[string]string // Tracks embed nesting depth for this render, guarding against a self- // or mutually-embedding file recursing without bound. embedDepth int // Directory (relative to the site root, or absolute once a nested embed // has narrowed it further) that the next <embed src="..."> encountered // in this page's own body should resolve against. NewZasData seeds this // to the rendered page's own directory, so an embed written inside a // subdirectory page resolves relative to that subdirectory, the same // way a relative <img src>/<a href> would once the page is deployed and // viewed in a browser - see resolveEmbedSrc in generate.go. Markdown // and Html save and restore this around their own recursive // parseAndReplace call so a chain of embeds each resolves relative to // the file that embedded it, not the outermost page. Generate // overrides it back to the site root before its own parseAndReplace // pass, since any <embed> still unresolved at that point was written // directly into layout.html - a single, fixed file shared by every // page - rather than into this page's own content, and layout-level // embeds (e.g. a site-wide footer) intentionally keep resolving // site-root-relative regardless of which page is currently rendering. embedBaseDir string}
// ZasSiteData is the site configuration.//// They are required fields in order to complete social/semantic meta tags.type ZasSiteData struct { BaseURL string Image string}
// Title returns the current title, from page's config and first level// header (H1), in this order.func (zd *ZasData) Title() (title string) { title, ok := zd.Page["title"].(string) if !ok { title = zd.FirstTitle } return}
// URL builds the URL from current configuration. BaseURL is documented as// being configured without a trailing slash, but nothing enforces that; a// trailing slash is trimmed here so it doesn't double up with Path, which// always starts with its own leading slash.func (zd *ZasData) URL() string { return strings.TrimRight(zd.Site.BaseURL, "/") + zd.Path}
// Extra is a helper template method to get any value from ZasData.config// using paths. It errors both when a path segment isn't itself a section// and when the final key is missing or isn't a string - the two ways a// keypath can fail to resolve to a real value.func (zd *ZasData) Extra(keypath string) (value string, err error) { keypath = path.Clean(keypath) if path.IsAbs(keypath) { keypath = keypath[1:] } steps := strings.Split(keypath, "/") last := len(steps) - 1 key, steps := steps[last], steps[:last] section := zd.config for _, step := range steps { section = section.GetSection(step) if section == nil { err = errors.New("not found") return } } var ok bool if value, ok = section.GetStringOK(key); !ok { err = errors.New("not found") } return}
// Language returns the page's resolved language.func (zd *ZasData) Language() (string, error) { return zd.Resolve("language")}
// Resolve resolves id from Page, then Directory, then site-wide Extra// config, in that order. It returns an error only when id is present in// Page or Directory but isn't a string (e.g. a page-config typo like// "language:" with no value, or a numeric value) — a genuinely absent key// still falls back to Extra's own lenient "" default.func (zd *ZasData) Resolve(id string) (string, error) { var ( value interface{} ok bool ) value, ok = zd.Page[id] if !ok { if zd.Directory != nil { value, ok = zd.Directory[id] } if !ok { s, _ := zd.Extra("/site/" + id) return s, nil } } s, isString := value.(string) if !isString { return "", fmt.Errorf("config value %q must be a string, got %T", id, value) } return s, nil}
// E translates s for the page's resolved language, falling back to// "**s**" when no translation is found.func (zd *ZasData) E(s string, a ...interface{}) (t string, err error) { lang, err := zd.Language() if err != nil { return "", err } zd.i18n.SetTarget(lang) t, err = zd.i18n.Translate(s, a...) if err != nil { t = "**" + s + "**" err = nil } return}
// H is like E but returns the translation as trusted HTML.func (zd *ZasData) H(s string, a ...interface{}) (h thtml.HTML, err error) { t, err := zd.E(s, a...) return thtml.HTML(t), err}
// IsHome reports whether the current page is the site's home page.func (zd *ZasData) IsHome() (bool, error) { lang, err := zd.Language() if err != nil { return false, err } return zd.Path == "/index.html" || zd.Path == fmt.Sprintf("/%s/index.html", lang), nil}
// NewZasData builds a ZasData for the page at srcPath.func NewZasData(srcPath string, gen *Generator) (data ZasData) { // Any path must finish in ".html". srcPath = swapExtension(srcPath, ".md", ".html") // filepath.Walk (the only caller) yields srcPath with the OS's own // separator, but data.Path becomes a URL - which always uses forward // slashes regardless of platform, so a nested page on Windows doesn't // end up with a literal backslash in its URL (and, via IsHome's // hardcoded "/"-separated comparisons below, so a language-prefixed // home page can still be recognized as one there too). data.Path = "/" + filepath.ToSlash(srcPath) // srcPath (not data.Path) here: it still has the OS's own separator, // which is what resolveEmbedSrc's filepath.Join expects, and its // directory portion is identical either way - swapExtension above only // ever rewrites the final path component's extension. data.embedBaseDir = filepath.Dir(srcPath) data.config = gen.Config // Each ZasData gets its own i18n.Build sharing the (read-only, post-init) // Index, so per-render SetTarget/Translate calls don't race or bleed // across languages on a Build shared by every render goroutine. data.i18n = &i18n.Build{ Index: gen.I18n.Index, Origin: gen.I18n.Origin, } data.Site.BaseURL = gen.Config.GetSection("site").GetString("baseurl") data.Site.Image = gen.Config.GetSection("site").GetString("image") return}