hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

transform.go (17097B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package resources
   15 
   16 import (
   17 	"bytes"
   18 	"fmt"
   19 	"image"
   20 	"io"
   21 	"path"
   22 	"strings"
   23 	"sync"
   24 
   25 	"github.com/gohugoio/hugo/common/paths"
   26 
   27 	"github.com/gohugoio/hugo/resources/images"
   28 	"github.com/gohugoio/hugo/resources/images/exif"
   29 	"github.com/spf13/afero"
   30 
   31 	bp "github.com/gohugoio/hugo/bufferpool"
   32 
   33 	"github.com/gohugoio/hugo/common/herrors"
   34 	"github.com/gohugoio/hugo/common/hugio"
   35 	"github.com/gohugoio/hugo/common/maps"
   36 	"github.com/gohugoio/hugo/helpers"
   37 	"github.com/gohugoio/hugo/resources/internal"
   38 	"github.com/gohugoio/hugo/resources/resource"
   39 
   40 	"github.com/gohugoio/hugo/media"
   41 )
   42 
   43 var (
   44 	_ resource.ContentResource        = (*resourceAdapter)(nil)
   45 	_ resourceCopier                  = (*resourceAdapter)(nil)
   46 	_ resource.ReadSeekCloserResource = (*resourceAdapter)(nil)
   47 	_ resource.Resource               = (*resourceAdapter)(nil)
   48 	_ resource.Source                 = (*resourceAdapter)(nil)
   49 	_ resource.Identifier             = (*resourceAdapter)(nil)
   50 	_ resource.ResourceMetaProvider   = (*resourceAdapter)(nil)
   51 )
   52 
   53 // These are transformations that need special support in Hugo that may not
   54 // be available when building the theme/site so we write the transformation
   55 // result to disk and reuse if needed for these,
   56 // TODO(bep) it's a little fragile having these constants redefined here.
   57 var transformationsToCacheOnDisk = map[string]bool{
   58 	"postcss":    true,
   59 	"tocss":      true,
   60 	"tocss-dart": true,
   61 }
   62 
   63 func newResourceAdapter(spec *Spec, lazyPublish bool, target transformableResource) *resourceAdapter {
   64 	var po *publishOnce
   65 	if lazyPublish {
   66 		po = &publishOnce{}
   67 	}
   68 	return &resourceAdapter{
   69 		resourceTransformations: &resourceTransformations{},
   70 		resourceAdapterInner: &resourceAdapterInner{
   71 			spec:        spec,
   72 			publishOnce: po,
   73 			target:      target,
   74 		},
   75 	}
   76 }
   77 
   78 // ResourceTransformation is the interface that a resource transformation step
   79 // needs to implement.
   80 type ResourceTransformation interface {
   81 	Key() internal.ResourceTransformationKey
   82 	Transform(ctx *ResourceTransformationCtx) error
   83 }
   84 
   85 type ResourceTransformationCtx struct {
   86 	// The content to transform.
   87 	From io.Reader
   88 
   89 	// The target of content transformation.
   90 	// The current implementation requires that r is written to w
   91 	// even if no transformation is performed.
   92 	To io.Writer
   93 
   94 	// This is the relative path to the original source. Unix styled slashes.
   95 	SourcePath string
   96 
   97 	// This is the relative target path to the resource. Unix styled slashes.
   98 	InPath string
   99 
  100 	// The relative target path to the transformed resource. Unix styled slashes.
  101 	OutPath string
  102 
  103 	// The input media type
  104 	InMediaType media.Type
  105 
  106 	// The media type of the transformed resource.
  107 	OutMediaType media.Type
  108 
  109 	// Data data can be set on the transformed Resource. Not that this need
  110 	// to be simple types, as it needs to be serialized to JSON and back.
  111 	Data map[string]any
  112 
  113 	// This is used to publish additional artifacts, e.g. source maps.
  114 	// We may improve this.
  115 	OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error)
  116 }
  117 
  118 // AddOutPathIdentifier transforming InPath to OutPath adding an identifier,
  119 // eg '.min' before any extension.
  120 func (ctx *ResourceTransformationCtx) AddOutPathIdentifier(identifier string) {
  121 	ctx.OutPath = ctx.addPathIdentifier(ctx.InPath, identifier)
  122 }
  123 
  124 // PublishSourceMap writes the content to the target folder of the main resource
  125 // with the ".map" extension added.
  126 func (ctx *ResourceTransformationCtx) PublishSourceMap(content string) error {
  127 	target := ctx.OutPath + ".map"
  128 	f, err := ctx.OpenResourcePublisher(target)
  129 	if err != nil {
  130 		return err
  131 	}
  132 	defer f.Close()
  133 	_, err = f.Write([]byte(content))
  134 	return err
  135 }
  136 
  137 // ReplaceOutPathExtension transforming InPath to OutPath replacing the file
  138 // extension, e.g. ".scss"
  139 func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) {
  140 	dir, file := path.Split(ctx.InPath)
  141 	base, _ := paths.PathAndExt(file)
  142 	ctx.OutPath = path.Join(dir, (base + newExt))
  143 }
  144 
  145 func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string {
  146 	dir, file := path.Split(inPath)
  147 	base, ext := paths.PathAndExt(file)
  148 	return path.Join(dir, (base + identifier + ext))
  149 }
  150 
  151 type publishOnce struct {
  152 	publisherInit sync.Once
  153 	publisherErr  error
  154 }
  155 
  156 type resourceAdapter struct {
  157 	commonResource
  158 	*resourceTransformations
  159 	*resourceAdapterInner
  160 }
  161 
  162 func (r *resourceAdapter) Content() (any, error) {
  163 	r.init(false, true)
  164 	if r.transformationsErr != nil {
  165 		return nil, r.transformationsErr
  166 	}
  167 	return r.target.Content()
  168 }
  169 
  170 func (r *resourceAdapter) Err() resource.ResourceError {
  171 	return nil
  172 }
  173 
  174 func (r *resourceAdapter) Data() any {
  175 	r.init(false, false)
  176 	return r.target.Data()
  177 }
  178 
  179 func (r resourceAdapter) cloneTo(targetPath string) resource.Resource {
  180 	newtTarget := r.target.cloneTo(targetPath)
  181 	newInner := &resourceAdapterInner{
  182 		spec:   r.spec,
  183 		target: newtTarget.(transformableResource),
  184 	}
  185 	if r.resourceAdapterInner.publishOnce != nil {
  186 		newInner.publishOnce = &publishOnce{}
  187 	}
  188 	r.resourceAdapterInner = newInner
  189 	return &r
  190 }
  191 
  192 func (r *resourceAdapter) Crop(spec string) (images.ImageResource, error) {
  193 	return r.getImageOps().Crop(spec)
  194 }
  195 
  196 func (r *resourceAdapter) Fill(spec string) (images.ImageResource, error) {
  197 	return r.getImageOps().Fill(spec)
  198 }
  199 
  200 func (r *resourceAdapter) Fit(spec string) (images.ImageResource, error) {
  201 	return r.getImageOps().Fit(spec)
  202 }
  203 
  204 func (r *resourceAdapter) Filter(filters ...any) (images.ImageResource, error) {
  205 	return r.getImageOps().Filter(filters...)
  206 }
  207 
  208 func (r *resourceAdapter) Height() int {
  209 	return r.getImageOps().Height()
  210 }
  211 
  212 func (r *resourceAdapter) Exif() *exif.ExifInfo {
  213 	return r.getImageOps().Exif()
  214 }
  215 
  216 func (r *resourceAdapter) Key() string {
  217 	r.init(false, false)
  218 	return r.target.(resource.Identifier).Key()
  219 }
  220 
  221 func (r *resourceAdapter) MediaType() media.Type {
  222 	r.init(false, false)
  223 	return r.target.MediaType()
  224 }
  225 
  226 func (r *resourceAdapter) Name() string {
  227 	r.init(false, false)
  228 	return r.target.Name()
  229 }
  230 
  231 func (r *resourceAdapter) Params() maps.Params {
  232 	r.init(false, false)
  233 	return r.target.Params()
  234 }
  235 
  236 func (r *resourceAdapter) Permalink() string {
  237 	r.init(true, false)
  238 	return r.target.Permalink()
  239 }
  240 
  241 func (r *resourceAdapter) Publish() error {
  242 	r.init(false, false)
  243 
  244 	return r.target.Publish()
  245 }
  246 
  247 func (r *resourceAdapter) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
  248 	r.init(false, false)
  249 	return r.target.ReadSeekCloser()
  250 }
  251 
  252 func (r *resourceAdapter) RelPermalink() string {
  253 	r.init(true, false)
  254 	return r.target.RelPermalink()
  255 }
  256 
  257 func (r *resourceAdapter) Resize(spec string) (images.ImageResource, error) {
  258 	return r.getImageOps().Resize(spec)
  259 }
  260 
  261 func (r *resourceAdapter) ResourceType() string {
  262 	r.init(false, false)
  263 	return r.target.ResourceType()
  264 }
  265 
  266 func (r *resourceAdapter) String() string {
  267 	return r.Name()
  268 }
  269 
  270 func (r *resourceAdapter) Title() string {
  271 	r.init(false, false)
  272 	return r.target.Title()
  273 }
  274 
  275 func (r resourceAdapter) Transform(t ...ResourceTransformation) (ResourceTransformer, error) {
  276 	r.resourceTransformations = &resourceTransformations{
  277 		transformations: append(r.transformations, t...),
  278 	}
  279 
  280 	r.resourceAdapterInner = &resourceAdapterInner{
  281 		spec:        r.spec,
  282 		publishOnce: &publishOnce{},
  283 		target:      r.target,
  284 	}
  285 
  286 	return &r, nil
  287 }
  288 
  289 func (r *resourceAdapter) Width() int {
  290 	return r.getImageOps().Width()
  291 }
  292 
  293 func (r *resourceAdapter) DecodeImage() (image.Image, error) {
  294 	return r.getImageOps().DecodeImage()
  295 }
  296 
  297 func (r *resourceAdapter) getImageOps() images.ImageResourceOps {
  298 	img, ok := r.target.(images.ImageResourceOps)
  299 	if !ok {
  300 		if r.MediaType().SubType == "svg" {
  301 			panic("this method is only available for raster images. To determine if an image is SVG, you can do {{ if eq .MediaType.SubType \"svg\" }}{{ end }}")
  302 		}
  303 		fmt.Println(r.MediaType().SubType)
  304 		panic("this method is only available for image resources")
  305 	}
  306 	r.init(false, false)
  307 	return img
  308 }
  309 
  310 func (r *resourceAdapter) getMetaAssigner() metaAssigner {
  311 	return r.target
  312 }
  313 
  314 func (r *resourceAdapter) getSpec() *Spec {
  315 	return r.spec
  316 }
  317 
  318 func (r *resourceAdapter) publish() {
  319 	if r.publishOnce == nil {
  320 		return
  321 	}
  322 
  323 	r.publisherInit.Do(func() {
  324 		r.publisherErr = r.target.Publish()
  325 
  326 		if r.publisherErr != nil {
  327 			r.spec.Logger.Errorf("Failed to publish Resource: %s", r.publisherErr)
  328 		}
  329 	})
  330 }
  331 
  332 func (r *resourceAdapter) TransformationKey() string {
  333 	// Files with a suffix will be stored in cache (both on disk and in memory)
  334 	// partitioned by their suffix.
  335 	var key string
  336 	for _, tr := range r.transformations {
  337 		key = key + "_" + tr.Key().Value()
  338 	}
  339 
  340 	base := ResourceCacheKey(r.target.Key())
  341 	return r.spec.ResourceCache.cleanKey(base) + "_" + helpers.MD5String(key)
  342 }
  343 
  344 func (r *resourceAdapter) transform(publish, setContent bool) error {
  345 	cache := r.spec.ResourceCache
  346 
  347 	key := r.TransformationKey()
  348 
  349 	cached, found := cache.get(key)
  350 
  351 	if found {
  352 		r.resourceAdapterInner = cached.(*resourceAdapterInner)
  353 		return nil
  354 	}
  355 
  356 	// Acquire a write lock for the named transformation.
  357 	cache.nlocker.Lock(key)
  358 	// Check the cache again.
  359 	cached, found = cache.get(key)
  360 	if found {
  361 		r.resourceAdapterInner = cached.(*resourceAdapterInner)
  362 		cache.nlocker.Unlock(key)
  363 		return nil
  364 	}
  365 
  366 	defer cache.nlocker.Unlock(key)
  367 	defer cache.set(key, r.resourceAdapterInner)
  368 
  369 	b1 := bp.GetBuffer()
  370 	b2 := bp.GetBuffer()
  371 	defer bp.PutBuffer(b1)
  372 	defer bp.PutBuffer(b2)
  373 
  374 	tctx := &ResourceTransformationCtx{
  375 		Data:                  make(map[string]any),
  376 		OpenResourcePublisher: r.target.openPublishFileForWriting,
  377 	}
  378 
  379 	tctx.InMediaType = r.target.MediaType()
  380 	tctx.OutMediaType = r.target.MediaType()
  381 
  382 	startCtx := *tctx
  383 	updates := &transformationUpdate{startCtx: startCtx}
  384 
  385 	var contentrc hugio.ReadSeekCloser
  386 
  387 	contentrc, err := contentReadSeekerCloser(r.target)
  388 	if err != nil {
  389 		return err
  390 	}
  391 
  392 	defer contentrc.Close()
  393 
  394 	tctx.From = contentrc
  395 	tctx.To = b1
  396 
  397 	tctx.InPath = r.target.TargetPath()
  398 	tctx.SourcePath = tctx.InPath
  399 
  400 	counter := 0
  401 	writeToFileCache := false
  402 
  403 	var transformedContentr io.Reader
  404 
  405 	for i, tr := range r.transformations {
  406 		if i != 0 {
  407 			tctx.InMediaType = tctx.OutMediaType
  408 		}
  409 
  410 		mayBeCachedOnDisk := transformationsToCacheOnDisk[tr.Key().Name]
  411 		if !writeToFileCache {
  412 			writeToFileCache = mayBeCachedOnDisk
  413 		}
  414 
  415 		if i > 0 {
  416 			hasWrites := tctx.To.(*bytes.Buffer).Len() > 0
  417 			if hasWrites {
  418 				counter++
  419 				// Switch the buffers
  420 				if counter%2 == 0 {
  421 					tctx.From = b2
  422 					b1.Reset()
  423 					tctx.To = b1
  424 				} else {
  425 					tctx.From = b1
  426 					b2.Reset()
  427 					tctx.To = b2
  428 				}
  429 			}
  430 		}
  431 
  432 		newErr := func(err error) error {
  433 			msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type())
  434 
  435 			if err == herrors.ErrFeatureNotAvailable {
  436 				var errMsg string
  437 				if tr.Key().Name == "postcss" {
  438 					// This transformation is not available in this
  439 					// Most likely because PostCSS is not installed.
  440 					errMsg = ". Check your PostCSS installation; install with \"npm install postcss-cli\". See https://gohugo.io/hugo-pipes/postcss/"
  441 				} else if tr.Key().Name == "tocss" {
  442 					errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS."
  443 				} else if tr.Key().Name == "tocss-dart" {
  444 					errMsg = ". You need dart-sass-embedded in your system $PATH."
  445 
  446 				} else if tr.Key().Name == "babel" {
  447 					errMsg = ". You need to install Babel, see https://gohugo.io/hugo-pipes/babel/"
  448 				}
  449 
  450 				return fmt.Errorf(msg+errMsg+": %w", err)
  451 			}
  452 
  453 			return fmt.Errorf(msg+": %w", err)
  454 		}
  455 
  456 		var tryFileCache bool
  457 
  458 		if mayBeCachedOnDisk && r.spec.BuildConfig.UseResourceCache(nil) {
  459 			tryFileCache = true
  460 		} else {
  461 			err = tr.Transform(tctx)
  462 			if err != nil && err != herrors.ErrFeatureNotAvailable {
  463 				return newErr(err)
  464 			}
  465 
  466 			if mayBeCachedOnDisk {
  467 				tryFileCache = r.spec.BuildConfig.UseResourceCache(err)
  468 			}
  469 			if err != nil && !tryFileCache {
  470 				return newErr(err)
  471 			}
  472 		}
  473 
  474 		if tryFileCache {
  475 			f := r.target.tryTransformedFileCache(key, updates)
  476 			if f == nil {
  477 				if err != nil {
  478 					return newErr(err)
  479 				}
  480 				return newErr(fmt.Errorf("resource %q not found in file cache", key))
  481 			}
  482 			transformedContentr = f
  483 			updates.sourceFs = cache.fileCache.Fs
  484 			defer f.Close()
  485 
  486 			// The reader above is all we need.
  487 			break
  488 		}
  489 
  490 		if tctx.OutPath != "" {
  491 			tctx.InPath = tctx.OutPath
  492 			tctx.OutPath = ""
  493 		}
  494 	}
  495 
  496 	if transformedContentr == nil {
  497 		updates.updateFromCtx(tctx)
  498 	}
  499 
  500 	var publishwriters []io.WriteCloser
  501 
  502 	if publish {
  503 		publicw, err := r.target.openPublishFileForWriting(updates.targetPath)
  504 		if err != nil {
  505 			return err
  506 		}
  507 		publishwriters = append(publishwriters, publicw)
  508 	}
  509 
  510 	if transformedContentr == nil {
  511 		if writeToFileCache {
  512 			// Also write it to the cache
  513 			fi, metaw, err := cache.writeMeta(key, updates.toTransformedResourceMetadata())
  514 			if err != nil {
  515 				return err
  516 			}
  517 			updates.sourceFilename = &fi.Name
  518 			updates.sourceFs = cache.fileCache.Fs
  519 			publishwriters = append(publishwriters, metaw)
  520 		}
  521 
  522 		// Any transformations reading from From must also write to To.
  523 		// This means that if the target buffer is empty, we can just reuse
  524 		// the original reader.
  525 		if b, ok := tctx.To.(*bytes.Buffer); ok && b.Len() > 0 {
  526 			transformedContentr = tctx.To.(*bytes.Buffer)
  527 		} else {
  528 			transformedContentr = contentrc
  529 		}
  530 	}
  531 
  532 	// Also write it to memory
  533 	var contentmemw *bytes.Buffer
  534 
  535 	setContent = setContent || !writeToFileCache
  536 
  537 	if setContent {
  538 		contentmemw = bp.GetBuffer()
  539 		defer bp.PutBuffer(contentmemw)
  540 		publishwriters = append(publishwriters, hugio.ToWriteCloser(contentmemw))
  541 	}
  542 
  543 	publishw := hugio.NewMultiWriteCloser(publishwriters...)
  544 	_, err = io.Copy(publishw, transformedContentr)
  545 	if err != nil {
  546 		return err
  547 	}
  548 	publishw.Close()
  549 
  550 	if setContent {
  551 		s := contentmemw.String()
  552 		updates.content = &s
  553 	}
  554 
  555 	newTarget, err := r.target.cloneWithUpdates(updates)
  556 	if err != nil {
  557 		return err
  558 	}
  559 	r.target = newTarget
  560 
  561 	return nil
  562 }
  563 
  564 func (r *resourceAdapter) init(publish, setContent bool) {
  565 	r.initTransform(publish, setContent)
  566 }
  567 
  568 func (r *resourceAdapter) initTransform(publish, setContent bool) {
  569 	r.transformationsInit.Do(func() {
  570 		if len(r.transformations) == 0 {
  571 			// Nothing to do.
  572 			return
  573 		}
  574 
  575 		if publish {
  576 			// The transformation will write the content directly to
  577 			// the destination.
  578 			r.publishOnce = nil
  579 		}
  580 
  581 		r.transformationsErr = r.transform(publish, setContent)
  582 		if r.transformationsErr != nil {
  583 			if r.spec.ErrorSender != nil {
  584 				r.spec.ErrorSender.SendError(r.transformationsErr)
  585 			} else {
  586 				r.spec.Logger.Errorf("Transformation failed: %s", r.transformationsErr)
  587 			}
  588 		}
  589 	})
  590 
  591 	if publish && r.publishOnce != nil {
  592 		r.publish()
  593 	}
  594 }
  595 
  596 type resourceAdapterInner struct {
  597 	target transformableResource
  598 
  599 	spec *Spec
  600 
  601 	// Handles publishing (to /public) if needed.
  602 	*publishOnce
  603 }
  604 
  605 type resourceTransformations struct {
  606 	transformationsInit sync.Once
  607 	transformationsErr  error
  608 	transformations     []ResourceTransformation
  609 }
  610 
  611 type transformableResource interface {
  612 	baseResourceInternal
  613 
  614 	resource.ContentProvider
  615 	resource.Resource
  616 	resource.Identifier
  617 	resourceCopier
  618 }
  619 
  620 type transformationUpdate struct {
  621 	content        *string
  622 	sourceFilename *string
  623 	sourceFs       afero.Fs
  624 	targetPath     string
  625 	mediaType      media.Type
  626 	data           map[string]any
  627 
  628 	startCtx ResourceTransformationCtx
  629 }
  630 
  631 func (u *transformationUpdate) isContentChanged() bool {
  632 	return u.content != nil || u.sourceFilename != nil
  633 }
  634 
  635 func (u *transformationUpdate) toTransformedResourceMetadata() transformedResourceMetadata {
  636 	return transformedResourceMetadata{
  637 		MediaTypeV: u.mediaType.Type(),
  638 		Target:     u.targetPath,
  639 		MetaData:   u.data,
  640 	}
  641 }
  642 
  643 func (u *transformationUpdate) updateFromCtx(ctx *ResourceTransformationCtx) {
  644 	u.targetPath = ctx.OutPath
  645 	u.mediaType = ctx.OutMediaType
  646 	u.data = ctx.Data
  647 	u.targetPath = ctx.InPath
  648 }
  649 
  650 // We will persist this information to disk.
  651 type transformedResourceMetadata struct {
  652 	Target     string         `json:"Target"`
  653 	MediaTypeV string         `json:"MediaType"`
  654 	MetaData   map[string]any `json:"Data"`
  655 }
  656 
  657 // contentReadSeekerCloser returns a ReadSeekerCloser if possible for a given Resource.
  658 func contentReadSeekerCloser(r resource.Resource) (hugio.ReadSeekCloser, error) {
  659 	switch rr := r.(type) {
  660 	case resource.ReadSeekCloserResource:
  661 		rc, err := rr.ReadSeekCloser()
  662 		if err != nil {
  663 			return nil, err
  664 		}
  665 		return rc, nil
  666 	default:
  667 		return nil, fmt.Errorf("cannot transform content of Resource of type %T", r)
  668 
  669 	}
  670 }