github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/config/commonConfig.go (about)

     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 config
    15  
    16  import (
    17  	"fmt"
    18  	"regexp"
    19  	"sort"
    20  	"strings"
    21  
    22  	"github.com/bep/logg"
    23  	"github.com/gobwas/glob"
    24  	"github.com/gohugoio/hugo/common/loggers"
    25  	"github.com/gohugoio/hugo/common/types"
    26  
    27  	"github.com/gohugoio/hugo/common/herrors"
    28  	"github.com/mitchellh/mapstructure"
    29  	"github.com/spf13/cast"
    30  )
    31  
    32  type BaseConfig struct {
    33  	WorkingDir string
    34  	CacheDir   string
    35  	ThemesDir  string
    36  	PublishDir string
    37  }
    38  
    39  type CommonDirs struct {
    40  	// The directory where Hugo will look for themes.
    41  	ThemesDir string
    42  
    43  	// Where to put the generated files.
    44  	PublishDir string
    45  
    46  	// The directory to put the generated resources files. This directory should in most situations be considered temporary
    47  	// and not be committed to version control. But there may be cached content in here that you want to keep,
    48  	// e.g. resources/_gen/images for performance reasons or CSS built from SASS when your CI server doesn't have the full setup.
    49  	ResourceDir string
    50  
    51  	// The project root directory.
    52  	WorkingDir string
    53  
    54  	// The root directory for all cache files.
    55  	CacheDir string
    56  
    57  	// The content source directory.
    58  	// Deprecated: Use module mounts.
    59  	ContentDir string
    60  	// Deprecated: Use module mounts.
    61  	// The data source directory.
    62  	DataDir string
    63  	// Deprecated: Use module mounts.
    64  	// The layout source directory.
    65  	LayoutDir string
    66  	// Deprecated: Use module mounts.
    67  	// The i18n source directory.
    68  	I18nDir string
    69  	// Deprecated: Use module mounts.
    70  	// The archetypes source directory.
    71  	ArcheTypeDir string
    72  	// Deprecated: Use module mounts.
    73  	// The assets source directory.
    74  	AssetDir string
    75  }
    76  
    77  type LoadConfigResult struct {
    78  	Cfg         Provider
    79  	ConfigFiles []string
    80  	BaseConfig  BaseConfig
    81  }
    82  
    83  var defaultBuild = BuildConfig{
    84  	UseResourceCacheWhen: "fallback",
    85  	BuildStats:           BuildStats{},
    86  
    87  	CacheBusters: []CacheBuster{
    88  		{
    89  			Source: `assets/.*\.(js|ts|jsx|tsx)`,
    90  			Target: `(js|scripts|javascript)`,
    91  		},
    92  		{
    93  			Source: `assets/.*\.(css|sass|scss)$`,
    94  			Target: cssTargetCachebusterRe,
    95  		},
    96  		{
    97  			Source: `(postcss|tailwind)\.config\.js`,
    98  			Target: cssTargetCachebusterRe,
    99  		},
   100  		// This is deliberately coarse grained; it will cache bust resources with "json" in the cache key when js files changes, which is good.
   101  		{
   102  			Source: `assets/.*\.(.*)$`,
   103  			Target: `$1`,
   104  		},
   105  	},
   106  }
   107  
   108  // BuildConfig holds some build related configuration.
   109  type BuildConfig struct {
   110  	UseResourceCacheWhen string // never, fallback, always. Default is fallback
   111  
   112  	// When enabled, will collect and write a hugo_stats.json with some build
   113  	// related aggregated data (e.g. CSS class names).
   114  	// Note that this was a bool <= v0.115.0.
   115  	BuildStats BuildStats
   116  
   117  	// Can be used to toggle off writing of the IntelliSense /assets/jsconfig.js
   118  	// file.
   119  	NoJSConfigInAssets bool
   120  
   121  	// Can used to control how the resource cache gets evicted on rebuilds.
   122  	CacheBusters []CacheBuster
   123  }
   124  
   125  // BuildStats configures if and what to write to the hugo_stats.json file.
   126  type BuildStats struct {
   127  	Enable         bool
   128  	DisableTags    bool
   129  	DisableClasses bool
   130  	DisableIDs     bool
   131  }
   132  
   133  func (w BuildStats) Enabled() bool {
   134  	if !w.Enable {
   135  		return false
   136  	}
   137  	return !w.DisableTags || !w.DisableClasses || !w.DisableIDs
   138  }
   139  
   140  func (b BuildConfig) clone() BuildConfig {
   141  	b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...)
   142  	return b
   143  }
   144  
   145  func (b BuildConfig) UseResourceCache(err error) bool {
   146  	if b.UseResourceCacheWhen == "never" {
   147  		return false
   148  	}
   149  
   150  	if b.UseResourceCacheWhen == "fallback" {
   151  		return herrors.IsFeatureNotAvailableError(err)
   152  	}
   153  
   154  	return true
   155  }
   156  
   157  // MatchCacheBuster returns the cache buster for the given path p, nil if none.
   158  func (s BuildConfig) MatchCacheBuster(logger loggers.Logger, p string) (func(string) bool, error) {
   159  	var matchers []func(string) bool
   160  	for _, cb := range s.CacheBusters {
   161  		if matcher := cb.compiledSource(p); matcher != nil {
   162  			matchers = append(matchers, matcher)
   163  		}
   164  	}
   165  	if len(matchers) > 0 {
   166  		return (func(cacheKey string) bool {
   167  			for _, m := range matchers {
   168  				if m(cacheKey) {
   169  					return true
   170  				}
   171  			}
   172  			return false
   173  		}), nil
   174  	}
   175  	return nil, nil
   176  }
   177  
   178  func (b *BuildConfig) CompileConfig(logger loggers.Logger) error {
   179  	for i, cb := range b.CacheBusters {
   180  		if err := cb.CompileConfig(logger); err != nil {
   181  			return fmt.Errorf("failed to compile cache buster %q: %w", cb.Source, err)
   182  		}
   183  		b.CacheBusters[i] = cb
   184  	}
   185  	return nil
   186  }
   187  
   188  func DecodeBuildConfig(cfg Provider) BuildConfig {
   189  	m := cfg.GetStringMap("build")
   190  
   191  	b := defaultBuild.clone()
   192  	if m == nil {
   193  		return b
   194  	}
   195  
   196  	// writeStats was a bool <= v0.115.0.
   197  	if writeStats, ok := m["writestats"]; ok {
   198  		if bb, ok := writeStats.(bool); ok {
   199  			m["buildstats"] = BuildStats{Enable: bb}
   200  		}
   201  	}
   202  
   203  	err := mapstructure.WeakDecode(m, &b)
   204  	if err != nil {
   205  		return b
   206  	}
   207  
   208  	b.UseResourceCacheWhen = strings.ToLower(b.UseResourceCacheWhen)
   209  	when := b.UseResourceCacheWhen
   210  	if when != "never" && when != "always" && when != "fallback" {
   211  		b.UseResourceCacheWhen = "fallback"
   212  	}
   213  
   214  	return b
   215  }
   216  
   217  // SitemapConfig configures the sitemap to be generated.
   218  type SitemapConfig struct {
   219  	// The page change frequency.
   220  	ChangeFreq string
   221  	// The priority of the page.
   222  	Priority float64
   223  	// The sitemap filename.
   224  	Filename string
   225  }
   226  
   227  func DecodeSitemap(prototype SitemapConfig, input map[string]any) (SitemapConfig, error) {
   228  	err := mapstructure.WeakDecode(input, &prototype)
   229  	return prototype, err
   230  }
   231  
   232  // Config for the dev server.
   233  type Server struct {
   234  	Headers   []Headers
   235  	Redirects []Redirect
   236  
   237  	compiledHeaders   []glob.Glob
   238  	compiledRedirects []glob.Glob
   239  }
   240  
   241  func (s *Server) CompileConfig(logger loggers.Logger) error {
   242  	if s.compiledHeaders != nil {
   243  		return nil
   244  	}
   245  	for _, h := range s.Headers {
   246  		s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For))
   247  	}
   248  	for _, r := range s.Redirects {
   249  		s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From))
   250  	}
   251  
   252  	return nil
   253  }
   254  
   255  func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr {
   256  	if s.compiledHeaders == nil {
   257  		return nil
   258  	}
   259  
   260  	var matches []types.KeyValueStr
   261  
   262  	for i, g := range s.compiledHeaders {
   263  		if g.Match(pattern) {
   264  			h := s.Headers[i]
   265  			for k, v := range h.Values {
   266  				matches = append(matches, types.KeyValueStr{Key: k, Value: cast.ToString(v)})
   267  			}
   268  		}
   269  	}
   270  
   271  	sort.Slice(matches, func(i, j int) bool {
   272  		return matches[i].Key < matches[j].Key
   273  	})
   274  
   275  	return matches
   276  }
   277  
   278  func (s *Server) MatchRedirect(pattern string) Redirect {
   279  	if s.compiledRedirects == nil {
   280  		return Redirect{}
   281  	}
   282  
   283  	pattern = strings.TrimSuffix(pattern, "index.html")
   284  
   285  	for i, g := range s.compiledRedirects {
   286  		redir := s.Redirects[i]
   287  
   288  		// No redirect to self.
   289  		if redir.To == pattern {
   290  			return Redirect{}
   291  		}
   292  
   293  		if g.Match(pattern) {
   294  			return redir
   295  		}
   296  	}
   297  
   298  	return Redirect{}
   299  }
   300  
   301  type Headers struct {
   302  	For    string
   303  	Values map[string]any
   304  }
   305  
   306  type Redirect struct {
   307  	From string
   308  	To   string
   309  
   310  	// HTTP status code to use for the redirect.
   311  	// A status code of 200 will trigger a URL rewrite.
   312  	Status int
   313  
   314  	// Forcode redirect, even if original request path exists.
   315  	Force bool
   316  }
   317  
   318  // CacheBuster configures cache busting for assets.
   319  type CacheBuster struct {
   320  	// Trigger for files matching this regexp.
   321  	Source string
   322  
   323  	// Cache bust targets matching this regexp.
   324  	// This regexp can contain group matches (e.g. $1) from the source regexp.
   325  	Target string
   326  
   327  	compiledSource func(string) func(string) bool
   328  }
   329  
   330  func (c *CacheBuster) CompileConfig(logger loggers.Logger) error {
   331  	if c.compiledSource != nil {
   332  		return nil
   333  	}
   334  
   335  	source := c.Source
   336  	sourceRe, err := regexp.Compile(source)
   337  	if err != nil {
   338  		return fmt.Errorf("failed to compile cache buster source %q: %w", c.Source, err)
   339  	}
   340  	target := c.Target
   341  	var compileErr error
   342  	debugl := logger.Logger().WithLevel(logg.LevelDebug).WithField(loggers.FieldNameCmd, "cachebuster")
   343  
   344  	c.compiledSource = func(s string) func(string) bool {
   345  		m := sourceRe.FindStringSubmatch(s)
   346  		matchString := "no match"
   347  		match := m != nil
   348  		if match {
   349  			matchString = "match!"
   350  		}
   351  		debugl.Logf("Matching %q with source %q: %s", s, source, matchString)
   352  		if !match {
   353  			return nil
   354  		}
   355  		groups := m[1:]
   356  		currentTarget := target
   357  		// Replace $1, $2 etc. in target.
   358  		for i, g := range groups {
   359  			currentTarget = strings.ReplaceAll(target, fmt.Sprintf("$%d", i+1), g)
   360  		}
   361  		targetRe, err := regexp.Compile(currentTarget)
   362  		if err != nil {
   363  			compileErr = fmt.Errorf("failed to compile cache buster target %q: %w", currentTarget, err)
   364  			return nil
   365  		}
   366  		return func(ss string) bool {
   367  			match = targetRe.MatchString(ss)
   368  			matchString := "no match"
   369  			if match {
   370  				matchString = "match!"
   371  			}
   372  			logger.Debugf("Matching %q with target %q: %s", ss, currentTarget, matchString)
   373  
   374  			return match
   375  		}
   376  
   377  	}
   378  	return compileErr
   379  }
   380  
   381  func (r Redirect) IsZero() bool {
   382  	return r.From == ""
   383  }
   384  
   385  const (
   386  	// Keep this a little coarse grained, some false positives are OK.
   387  	cssTargetCachebusterRe = `(css|styles|scss|sass)`
   388  )
   389  
   390  func DecodeServer(cfg Provider) (Server, error) {
   391  	s := &Server{}
   392  
   393  	_ = mapstructure.WeakDecode(cfg.GetStringMap("server"), s)
   394  
   395  	for i, redir := range s.Redirects {
   396  		// Get it in line with the Hugo server for OK responses.
   397  		// We currently treat the 404 as a special case, they are always "ugly", so keep them as is.
   398  		if redir.Status != 404 {
   399  			redir.To = strings.TrimSuffix(redir.To, "index.html")
   400  			if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") {
   401  				// There are some tricky infinite loop situations when dealing
   402  				// when the target does not have a trailing slash.
   403  				// This can certainly be handled better, but not time for that now.
   404  				return Server{}, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
   405  			}
   406  		}
   407  		s.Redirects[i] = redir
   408  	}
   409  
   410  	if len(s.Redirects) == 0 {
   411  		// Set up a default redirect for 404s.
   412  		s.Redirects = []Redirect{
   413  			{
   414  				From:   "**",
   415  				To:     "/404.html",
   416  				Status: 404,
   417  			},
   418  		}
   419  
   420  	}
   421  
   422  	return *s, nil
   423  }