mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
dea71670c0
Before this commit, you would have to use page bundles to do image processing etc. in Hugo. This commit adds * A new `/assets` top-level project or theme dir (configurable via `assetDir`) * A new template func, `resources.Get` which can be used to "get a resource" that can be further processed. This means that you can now do this in your templates (or shortcodes): ```bash {{ $sunset := (resources.Get "images/sunset.jpg").Fill "300x200" }} ``` This also adds a new `extended` build tag that enables powerful SCSS/SASS support with source maps. To compile this from source, you will also need a C compiler installed: ``` HUGO_BUILD_TAGS=extended mage install ``` Note that you can use output of the SCSS processing later in a non-SCSSS-enabled Hugo. The `SCSS` processor is a _Resource transformation step_ and it can be chained with the many others in a pipeline: ```bash {{ $css := resources.Get "styles.scss" | resources.ToCSS | resources.PostCSS | resources.Minify | resources.Fingerprint }} <link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Digest }}" media="screen"> ``` The transformation funcs above have aliases, so it can be shortened to: ```bash {{ $css := resources.Get "styles.scss" | toCSS | postCSS | minify | fingerprint }} <link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Digest }}" media="screen"> ``` A quick tip would be to avoid the fingerprinting part, and possibly also the not-superfast `postCSS` when you're doing development, as it allows Hugo to be smarter about the rebuilding. Documentation will follow, but have a look at the demo repo in https://github.com/bep/hugo-sass-test New functions to create `Resource` objects: * `resources.Get` (see above) * `resources.FromString`: Create a Resource from a string. New `Resource` transformation funcs: * `resources.ToCSS`: Compile `SCSS` or `SASS` into `CSS`. * `resources.PostCSS`: Process your CSS with PostCSS. Config file support (project or theme or passed as an option). * `resources.Minify`: Currently supports `css`, `js`, `json`, `html`, `svg`, `xml`. * `resources.Fingerprint`: Creates a fingerprinted version of the given Resource with Subresource Integrity.. * `resources.Concat`: Concatenates a list of Resource objects. Think of this as a poor man's bundler. * `resources.ExecuteAsTemplate`: Parses and executes the given Resource and data context (e.g. .Site) as a Go template. Fixes #4381 Fixes #4903 Fixes #4858
359 lines
7.8 KiB
Go
359 lines
7.8 KiB
Go
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package hugolib
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
"github.com/gohugoio/hugo/resource"
|
|
)
|
|
|
|
var (
|
|
// This should be the only list of valid extensions for content files.
|
|
contentFileExtensions = []string{
|
|
"html", "htm",
|
|
"mdown", "markdown", "md",
|
|
"asciidoc", "adoc", "ad",
|
|
"rest", "rst",
|
|
"mmark",
|
|
"org",
|
|
"pandoc", "pdc"}
|
|
|
|
contentFileExtensionsSet map[string]bool
|
|
)
|
|
|
|
func init() {
|
|
contentFileExtensionsSet = make(map[string]bool)
|
|
for _, ext := range contentFileExtensions {
|
|
contentFileExtensionsSet[ext] = true
|
|
}
|
|
}
|
|
|
|
func newHandlerChain(s *Site) contentHandler {
|
|
c := &contentHandlers{s: s}
|
|
|
|
contentFlow := c.parsePage(c.processFirstMatch(
|
|
// Handles all files with a content file extension. See above.
|
|
c.handlePageContent(),
|
|
|
|
// Every HTML file without front matter will be passed on to this handler.
|
|
c.handleHTMLContent(),
|
|
))
|
|
|
|
c.rootHandler = c.processFirstMatch(
|
|
contentFlow,
|
|
|
|
// Creates a file resource (image, CSS etc.) if there is a parent
|
|
// page set on the current context.
|
|
c.createResource(),
|
|
|
|
// Everything that isn't handled above, will just be copied
|
|
// to destination.
|
|
c.copyFile(),
|
|
)
|
|
|
|
return c.rootHandler
|
|
|
|
}
|
|
|
|
type contentHandlers struct {
|
|
s *Site
|
|
rootHandler contentHandler
|
|
}
|
|
|
|
func (c *contentHandlers) processFirstMatch(handlers ...contentHandler) func(ctx *handlerContext) handlerResult {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
for _, h := range handlers {
|
|
res := h(ctx)
|
|
if res.handled || res.err != nil {
|
|
return res
|
|
}
|
|
}
|
|
return handlerResult{err: errors.New("no matching handler found")}
|
|
}
|
|
}
|
|
|
|
type handlerContext struct {
|
|
// These are the pages stored in Site.
|
|
pages chan<- *Page
|
|
|
|
doNotAddToSiteCollections bool
|
|
|
|
currentPage *Page
|
|
parentPage *Page
|
|
|
|
bundle *bundleDir
|
|
|
|
source *fileInfo
|
|
|
|
// Relative path to the target.
|
|
target string
|
|
}
|
|
|
|
func (c *handlerContext) ext() string {
|
|
if c.currentPage != nil {
|
|
if c.currentPage.Markup != "" {
|
|
return c.currentPage.Markup
|
|
}
|
|
return c.currentPage.Ext()
|
|
}
|
|
|
|
if c.bundle != nil {
|
|
return c.bundle.fi.Ext()
|
|
} else {
|
|
return c.source.Ext()
|
|
}
|
|
}
|
|
|
|
func (c *handlerContext) targetPath() string {
|
|
if c.target != "" {
|
|
return c.target
|
|
}
|
|
|
|
return c.source.Filename()
|
|
}
|
|
|
|
func (c *handlerContext) file() *fileInfo {
|
|
if c.bundle != nil {
|
|
return c.bundle.fi
|
|
}
|
|
|
|
return c.source
|
|
}
|
|
|
|
// Create a copy with the current context as its parent.
|
|
func (c handlerContext) childCtx(fi *fileInfo) *handlerContext {
|
|
if c.currentPage == nil {
|
|
panic("Need a Page to create a child context")
|
|
}
|
|
|
|
c.target = strings.TrimPrefix(fi.Path(), c.bundle.fi.Dir())
|
|
c.source = fi
|
|
|
|
c.doNotAddToSiteCollections = c.bundle != nil && c.bundle.tp != bundleBranch
|
|
|
|
c.bundle = nil
|
|
|
|
c.parentPage = c.currentPage
|
|
c.currentPage = nil
|
|
|
|
return &c
|
|
}
|
|
|
|
func (c *handlerContext) supports(exts ...string) bool {
|
|
ext := c.ext()
|
|
for _, s := range exts {
|
|
if s == ext {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *handlerContext) isContentFile() bool {
|
|
return contentFileExtensionsSet[c.ext()]
|
|
}
|
|
|
|
type (
|
|
handlerResult struct {
|
|
err error
|
|
handled bool
|
|
resource resource.Resource
|
|
}
|
|
|
|
contentHandler func(ctx *handlerContext) handlerResult
|
|
)
|
|
|
|
var (
|
|
notHandled handlerResult
|
|
)
|
|
|
|
func (c *contentHandlers) parsePage(h contentHandler) contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
if !ctx.isContentFile() {
|
|
return notHandled
|
|
}
|
|
|
|
result := handlerResult{handled: true}
|
|
fi := ctx.file()
|
|
|
|
f, err := fi.Open()
|
|
if err != nil {
|
|
return handlerResult{err: fmt.Errorf("(%s) failed to open content file: %s", fi.Filename(), err)}
|
|
}
|
|
defer f.Close()
|
|
|
|
p := c.s.newPageFromFile(fi)
|
|
|
|
_, err = p.ReadFrom(f)
|
|
if err != nil {
|
|
return handlerResult{err: err}
|
|
}
|
|
|
|
if !p.shouldBuild() {
|
|
if !ctx.doNotAddToSiteCollections {
|
|
ctx.pages <- p
|
|
}
|
|
return result
|
|
}
|
|
|
|
ctx.currentPage = p
|
|
|
|
if ctx.bundle != nil {
|
|
// Add the bundled files
|
|
for _, fi := range ctx.bundle.resources {
|
|
childCtx := ctx.childCtx(fi)
|
|
res := c.rootHandler(childCtx)
|
|
if res.err != nil {
|
|
return res
|
|
}
|
|
if res.resource != nil {
|
|
if pageResource, ok := res.resource.(*Page); ok {
|
|
pageResource.resourcePath = filepath.ToSlash(childCtx.target)
|
|
pageResource.parent = p
|
|
}
|
|
p.Resources = append(p.Resources, res.resource)
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(p.Resources, func(i, j int) bool {
|
|
if p.Resources[i].ResourceType() < p.Resources[j].ResourceType() {
|
|
return true
|
|
}
|
|
|
|
p1, ok1 := p.Resources[i].(*Page)
|
|
p2, ok2 := p.Resources[j].(*Page)
|
|
|
|
if ok1 != ok2 {
|
|
return ok2
|
|
}
|
|
|
|
if ok1 {
|
|
return defaultPageSort(p1, p2)
|
|
}
|
|
|
|
return p.Resources[i].RelPermalink() < p.Resources[j].RelPermalink()
|
|
})
|
|
|
|
// Assign metadata from front matter if set
|
|
if len(p.resourcesMetadata) > 0 {
|
|
resource.AssignMetadata(p.resourcesMetadata, p.Resources...)
|
|
}
|
|
|
|
}
|
|
|
|
return h(ctx)
|
|
}
|
|
}
|
|
|
|
func (c *contentHandlers) handlePageContent() contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
if ctx.supports("html", "htm") {
|
|
return notHandled
|
|
}
|
|
|
|
p := ctx.currentPage
|
|
|
|
// Work on a copy of the raw content from now on.
|
|
p.createWorkContentCopy()
|
|
|
|
if err := p.processShortcodes(); err != nil {
|
|
p.s.Log.ERROR.Println(err)
|
|
}
|
|
|
|
if c.s.Cfg.GetBool("enableEmoji") {
|
|
p.workContent = helpers.Emojify(p.workContent)
|
|
}
|
|
|
|
p.workContent = p.replaceDivider(p.workContent)
|
|
p.workContent = p.renderContent(p.workContent)
|
|
|
|
tmpContent, tmpTableOfContents := helpers.ExtractTOC(p.workContent)
|
|
p.TableOfContents = helpers.BytesToHTML(tmpTableOfContents)
|
|
p.workContent = tmpContent
|
|
|
|
if !ctx.doNotAddToSiteCollections {
|
|
ctx.pages <- p
|
|
}
|
|
|
|
return handlerResult{handled: true, resource: p}
|
|
}
|
|
}
|
|
|
|
func (c *contentHandlers) handleHTMLContent() contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
if !ctx.supports("html", "htm") {
|
|
return notHandled
|
|
}
|
|
|
|
p := ctx.currentPage
|
|
|
|
p.createWorkContentCopy()
|
|
|
|
if err := p.processShortcodes(); err != nil {
|
|
p.s.Log.ERROR.Println(err)
|
|
}
|
|
|
|
if !ctx.doNotAddToSiteCollections {
|
|
ctx.pages <- p
|
|
}
|
|
|
|
return handlerResult{handled: true, resource: p}
|
|
}
|
|
}
|
|
|
|
func (c *contentHandlers) createResource() contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
if ctx.parentPage == nil {
|
|
return notHandled
|
|
}
|
|
|
|
resource, err := c.s.ResourceSpec.New(
|
|
resource.ResourceSourceDescriptor{
|
|
TargetPathBuilder: ctx.parentPage.subResourceTargetPathFactory,
|
|
SourceFile: ctx.source,
|
|
RelTargetFilename: ctx.target,
|
|
URLBase: c.s.GetURLLanguageBasePath(),
|
|
TargetPathBase: c.s.GetTargetLanguageBasePath(),
|
|
})
|
|
|
|
return handlerResult{err: err, handled: true, resource: resource}
|
|
}
|
|
}
|
|
|
|
func (c *contentHandlers) copyFile() contentHandler {
|
|
return func(ctx *handlerContext) handlerResult {
|
|
f, err := c.s.BaseFs.Content.Fs.Open(ctx.source.Filename())
|
|
if err != nil {
|
|
err := fmt.Errorf("failed to open file in copyFile: %s", err)
|
|
return handlerResult{err: err}
|
|
}
|
|
|
|
target := ctx.targetPath()
|
|
|
|
defer f.Close()
|
|
if err := c.s.publish(&c.s.PathSpec.ProcessingStats.Files, target, f); err != nil {
|
|
return handlerResult{err: err}
|
|
}
|
|
|
|
return handlerResult{handled: true}
|
|
}
|
|
}
|