github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/tpl/data/data.go (about) 1 // Copyright 2017 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 data provides template functions for working with external data 15 // sources. 16 package data 17 18 import ( 19 "bytes" 20 "encoding/csv" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "net/http" 25 "strings" 26 27 "github.com/gohugoio/hugo/common/maps" 28 "github.com/gohugoio/hugo/config/security" 29 30 "github.com/gohugoio/hugo/common/types" 31 32 "github.com/gohugoio/hugo/common/constants" 33 34 "github.com/spf13/cast" 35 36 "github.com/gohugoio/hugo/cache/filecache" 37 "github.com/gohugoio/hugo/deps" 38 ) 39 40 // New returns a new instance of the data-namespaced template functions. 41 func New(deps *deps.Deps) *Namespace { 42 return &Namespace{ 43 deps: deps, 44 cacheGetCSV: deps.ResourceSpec.FileCaches.GetCSVCache(), 45 cacheGetJSON: deps.ResourceSpec.FileCaches.GetJSONCache(), 46 client: http.DefaultClient, 47 } 48 } 49 50 // Namespace provides template functions for the "data" namespace. 51 type Namespace struct { 52 deps *deps.Deps 53 54 cacheGetJSON *filecache.Cache 55 cacheGetCSV *filecache.Cache 56 57 client *http.Client 58 } 59 60 // GetCSV expects the separator sep and one or n-parts of a URL to a resource which 61 // can either be a local or a remote one. 62 // The data separator can be a comma, semi-colon, pipe, etc, but only one character. 63 // If you provide multiple parts for the URL they will be joined together to the final URL. 64 // GetCSV returns nil or a slice slice to use in a short code. 65 func (ns *Namespace) GetCSV(sep string, args ...any) (d [][]string, err error) { 66 url, headers := toURLAndHeaders(args) 67 cache := ns.cacheGetCSV 68 69 unmarshal := func(b []byte) (bool, error) { 70 if d, err = parseCSV(b, sep); err != nil { 71 err = fmt.Errorf("failed to parse CSV file %s: %w", url, err) 72 73 return true, err 74 } 75 76 return false, nil 77 } 78 79 var req *http.Request 80 req, err = http.NewRequest("GET", url, nil) 81 if err != nil { 82 return nil, fmt.Errorf("failed to create request for getCSV for resource %s: %w", url, err) 83 } 84 85 // Add custom user headers. 86 addUserProvidedHeaders(headers, req) 87 addDefaultHeaders(req, "text/csv", "text/plain") 88 89 err = ns.getResource(cache, unmarshal, req) 90 if err != nil { 91 if security.IsAccessDenied(err) { 92 return nil, err 93 } 94 ns.deps.Log.Errorsf(constants.ErrRemoteGetCSV, "Failed to get CSV resource %q: %s", url, err) 95 return nil, nil 96 } 97 98 return 99 } 100 101 // GetJSON expects one or n-parts of a URL in args to a resource which can either be a local or a remote one. 102 // If you provide multiple parts they will be joined together to the final URL. 103 // GetJSON returns nil or parsed JSON to use in a short code. 104 func (ns *Namespace) GetJSON(args ...any) (any, error) { 105 var v any 106 url, headers := toURLAndHeaders(args) 107 cache := ns.cacheGetJSON 108 109 req, err := http.NewRequest("GET", url, nil) 110 if err != nil { 111 return nil, fmt.Errorf("Failed to create request for getJSON resource %s: %w", url, err) 112 } 113 114 unmarshal := func(b []byte) (bool, error) { 115 err := json.Unmarshal(b, &v) 116 if err != nil { 117 return true, err 118 } 119 return false, nil 120 } 121 122 addUserProvidedHeaders(headers, req) 123 addDefaultHeaders(req, "application/json") 124 125 err = ns.getResource(cache, unmarshal, req) 126 if err != nil { 127 if security.IsAccessDenied(err) { 128 return nil, err 129 } 130 ns.deps.Log.Errorsf(constants.ErrRemoteGetJSON, "Failed to get JSON resource %q: %s", url, err) 131 return nil, nil 132 } 133 134 return v, nil 135 } 136 137 func addDefaultHeaders(req *http.Request, accepts ...string) { 138 for _, accept := range accepts { 139 if !hasHeaderValue(req.Header, "Accept", accept) { 140 req.Header.Add("Accept", accept) 141 } 142 } 143 if !hasHeaderKey(req.Header, "User-Agent") { 144 req.Header.Add("User-Agent", "Hugo Static Site Generator") 145 } 146 } 147 148 func addUserProvidedHeaders(headers map[string]any, req *http.Request) { 149 if headers == nil { 150 return 151 } 152 for key, val := range headers { 153 vals := types.ToStringSlicePreserveString(val) 154 for _, s := range vals { 155 req.Header.Add(key, s) 156 } 157 } 158 } 159 160 func hasHeaderValue(m http.Header, key, value string) bool { 161 var s []string 162 var ok bool 163 164 if s, ok = m[key]; !ok { 165 return false 166 } 167 168 for _, v := range s { 169 if v == value { 170 return true 171 } 172 } 173 return false 174 } 175 176 func hasHeaderKey(m http.Header, key string) bool { 177 _, ok := m[key] 178 return ok 179 } 180 181 func toURLAndHeaders(urlParts []any) (string, map[string]any) { 182 if len(urlParts) == 0 { 183 return "", nil 184 } 185 186 // The last argument may be a map. 187 headers, err := maps.ToStringMapE(urlParts[len(urlParts)-1]) 188 if err == nil { 189 urlParts = urlParts[:len(urlParts)-1] 190 } else { 191 headers = nil 192 } 193 194 return strings.Join(cast.ToStringSlice(urlParts), ""), headers 195 } 196 197 // parseCSV parses bytes of CSV data into a slice slice string or an error 198 func parseCSV(c []byte, sep string) ([][]string, error) { 199 if len(sep) != 1 { 200 return nil, errors.New("Incorrect length of CSV separator: " + sep) 201 } 202 b := bytes.NewReader(c) 203 r := csv.NewReader(b) 204 rSep := []rune(sep) 205 r.Comma = rSep[0] 206 r.FieldsPerRecord = 0 207 return r.ReadAll() 208 }