github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/create/content.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 create provides functions to create new content. 15 package create 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "os" 22 "path/filepath" 23 "strings" 24 25 "github.com/gohugoio/hugo/hugofs/glob" 26 27 "github.com/gohugoio/hugo/common/hexec" 28 "github.com/gohugoio/hugo/common/paths" 29 30 "errors" 31 32 "github.com/gohugoio/hugo/hugofs/files" 33 34 "github.com/gohugoio/hugo/hugofs" 35 36 "github.com/gohugoio/hugo/helpers" 37 "github.com/gohugoio/hugo/hugolib" 38 "github.com/spf13/afero" 39 ) 40 41 const ( 42 // DefaultArchetypeTemplateTemplate is the template used in 'hugo new site' 43 // and the template we use as a fall back. 44 DefaultArchetypeTemplateTemplate = `--- 45 title: "{{ replace .File.ContentBaseName "-" " " | title }}" 46 date: {{ .Date }} 47 draft: true 48 --- 49 50 ` 51 ) 52 53 // NewContent creates a new content file in h (or a full bundle if the archetype is a directory) 54 // in targetPath. 55 func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error { 56 if h.BaseFs.Content.Dirs == nil { 57 return errors.New("no existing content directory configured for this project") 58 } 59 60 cf := hugolib.NewContentFactory(h) 61 62 if kind == "" { 63 var err error 64 kind, err = cf.SectionFromFilename(targetPath) 65 if err != nil { 66 return err 67 } 68 } 69 70 b := &contentBuilder{ 71 archeTypeFs: h.PathSpec.BaseFs.Archetypes.Fs, 72 sourceFs: h.PathSpec.Fs.Source, 73 ps: h.PathSpec, 74 h: h, 75 cf: cf, 76 77 kind: kind, 78 targetPath: targetPath, 79 force: force, 80 } 81 82 ext := paths.Ext(targetPath) 83 84 b.setArcheTypeFilenameToUse(ext) 85 86 withBuildLock := func() (string, error) { 87 unlock, err := h.BaseFs.LockBuild() 88 if err != nil { 89 return "", fmt.Errorf("failed to acquire a build lock: %s", err) 90 } 91 defer unlock() 92 93 if b.isDir { 94 return "", b.buildDir() 95 } 96 97 if ext == "" { 98 return "", fmt.Errorf("failed to resolve %q to an archetype template", targetPath) 99 } 100 101 if !files.IsContentFile(b.targetPath) { 102 return "", fmt.Errorf("target path %q is not a known content format", b.targetPath) 103 } 104 105 return b.buildFile() 106 107 } 108 109 filename, err := withBuildLock() 110 if err != nil { 111 return err 112 } 113 114 if filename != "" { 115 return b.openInEditorIfConfigured(filename) 116 } 117 118 return nil 119 120 } 121 122 type contentBuilder struct { 123 archeTypeFs afero.Fs 124 sourceFs afero.Fs 125 126 ps *helpers.PathSpec 127 h *hugolib.HugoSites 128 cf hugolib.ContentFactory 129 130 // Builder state 131 archetypeFilename string 132 targetPath string 133 kind string 134 isDir bool 135 dirMap archetypeMap 136 force bool 137 } 138 139 func (b *contentBuilder) buildDir() error { 140 // Split the dir into content files and the rest. 141 if err := b.mapArcheTypeDir(); err != nil { 142 return err 143 } 144 145 var contentTargetFilenames []string 146 var baseDir string 147 148 for _, fi := range b.dirMap.contentFiles { 149 targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().Path, b.archetypeFilename)) 150 abs, err := b.cf.CreateContentPlaceHolder(targetFilename, b.force) 151 if err != nil { 152 return err 153 } 154 if baseDir == "" { 155 baseDir = strings.TrimSuffix(abs, targetFilename) 156 } 157 158 contentTargetFilenames = append(contentTargetFilenames, abs) 159 } 160 161 var contentInclusionFilter *glob.FilenameFilter 162 if !b.dirMap.siteUsed { 163 // We don't need to build everything. 164 contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool { 165 filename = strings.TrimPrefix(filename, string(os.PathSeparator)) 166 for _, cn := range contentTargetFilenames { 167 if strings.Contains(cn, filename) { 168 return true 169 } 170 } 171 return false 172 }) 173 174 } 175 176 if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil { 177 return err 178 } 179 180 for i, filename := range contentTargetFilenames { 181 if err := b.applyArcheType(filename, b.dirMap.contentFiles[i].Meta().Path); err != nil { 182 return err 183 } 184 } 185 186 // Copy the rest as is. 187 for _, f := range b.dirMap.otherFiles { 188 meta := f.Meta() 189 filename := meta.Path 190 191 in, err := meta.Open() 192 if err != nil { 193 return fmt.Errorf("failed to open non-content file: %w", err) 194 } 195 196 targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(filename, b.archetypeFilename)) 197 targetDir := filepath.Dir(targetFilename) 198 199 if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) { 200 return fmt.Errorf("failed to create target directory for %q: %w", targetDir, err) 201 } 202 203 out, err := b.sourceFs.Create(targetFilename) 204 if err != nil { 205 return err 206 } 207 208 _, err = io.Copy(out, in) 209 if err != nil { 210 return err 211 } 212 213 in.Close() 214 out.Close() 215 } 216 217 b.h.Log.Printf("Content dir %q created", filepath.Join(baseDir, b.targetPath)) 218 219 return nil 220 } 221 222 func (b *contentBuilder) buildFile() (string, error) { 223 contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath, b.force) 224 if err != nil { 225 return "", err 226 } 227 228 usesSite, err := b.usesSiteVar(b.archetypeFilename) 229 if err != nil { 230 return "", err 231 } 232 233 var contentInclusionFilter *glob.FilenameFilter 234 if !usesSite { 235 // We don't need to build everything. 236 contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool { 237 filename = strings.TrimPrefix(filename, string(os.PathSeparator)) 238 return strings.Contains(contentPlaceholderAbsFilename, filename) 239 }) 240 } 241 242 if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil { 243 return "", err 244 } 245 246 if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFilename); err != nil { 247 return "", err 248 } 249 250 b.h.Log.Printf("Content %q created", contentPlaceholderAbsFilename) 251 252 return contentPlaceholderAbsFilename, nil 253 } 254 255 func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) { 256 var pathsToCheck []string 257 258 if b.kind != "" { 259 pathsToCheck = append(pathsToCheck, b.kind+ext) 260 } 261 262 pathsToCheck = append(pathsToCheck, "default"+ext) 263 264 for _, p := range pathsToCheck { 265 fi, err := b.archeTypeFs.Stat(p) 266 if err == nil { 267 b.archetypeFilename = p 268 b.isDir = fi.IsDir() 269 return 270 } 271 } 272 273 } 274 275 func (b *contentBuilder) applyArcheType(contentFilename, archetypeFilename string) error { 276 p := b.h.GetContentPage(contentFilename) 277 if p == nil { 278 panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename)) 279 } 280 281 f, err := b.sourceFs.Create(contentFilename) 282 if err != nil { 283 return err 284 } 285 defer f.Close() 286 287 if archetypeFilename == "" { 288 return b.cf.ApplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate) 289 } 290 291 return b.cf.ApplyArchetypeFilename(f, p, b.kind, archetypeFilename) 292 293 } 294 295 func (b *contentBuilder) mapArcheTypeDir() error { 296 var m archetypeMap 297 298 walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error { 299 if err != nil { 300 return err 301 } 302 303 if fi.IsDir() { 304 return nil 305 } 306 307 fil := fi.(hugofs.FileMetaInfo) 308 309 if files.IsContentFile(path) { 310 m.contentFiles = append(m.contentFiles, fil) 311 if !m.siteUsed { 312 m.siteUsed, err = b.usesSiteVar(path) 313 if err != nil { 314 return err 315 } 316 } 317 return nil 318 } 319 320 m.otherFiles = append(m.otherFiles, fil) 321 322 return nil 323 } 324 325 walkCfg := hugofs.WalkwayConfig{ 326 WalkFn: walkFn, 327 Fs: b.archeTypeFs, 328 Root: b.archetypeFilename, 329 } 330 331 w := hugofs.NewWalkway(walkCfg) 332 333 if err := w.Walk(); err != nil { 334 return fmt.Errorf("failed to walk archetype dir %q: %w", b.archetypeFilename, err) 335 } 336 337 b.dirMap = m 338 339 return nil 340 } 341 342 func (b *contentBuilder) openInEditorIfConfigured(filename string) error { 343 editor := b.h.Conf.NewContentEditor() 344 if editor == "" { 345 return nil 346 } 347 348 editorExec := strings.Fields(editor)[0] 349 editorFlags := strings.Fields(editor)[1:] 350 351 var args []any 352 for _, editorFlag := range editorFlags { 353 args = append(args, editorFlag) 354 } 355 args = append( 356 args, 357 filename, 358 hexec.WithStdin(os.Stdin), 359 hexec.WithStderr(os.Stderr), 360 hexec.WithStdout(os.Stdout), 361 ) 362 363 b.h.Log.Printf("Editing %q with %q ...\n", filename, editorExec) 364 365 cmd, err := b.h.Deps.ExecHelper.New(editorExec, args...) 366 if err != nil { 367 return err 368 } 369 370 return cmd.Run() 371 } 372 373 func (b *contentBuilder) usesSiteVar(filename string) (bool, error) { 374 if filename == "" { 375 return false, nil 376 } 377 bb, err := afero.ReadFile(b.archeTypeFs, filename) 378 if err != nil { 379 return false, fmt.Errorf("failed to open archetype file: %w", err) 380 } 381 382 return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil 383 384 } 385 386 type archetypeMap struct { 387 // These needs to be parsed and executed as Go templates. 388 contentFiles []hugofs.FileMetaInfo 389 // These are just copied to destination. 390 otherFiles []hugofs.FileMetaInfo 391 // If the templates needs a fully built site. This can potentially be 392 // expensive, so only do when needed. 393 siteUsed bool 394 }