convert.go (5369B)
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 commands 15 16 import ( 17 "bytes" 18 "fmt" 19 "path/filepath" 20 "strings" 21 "time" 22 23 "github.com/gohugoio/hugo/parser/pageparser" 24 25 "github.com/gohugoio/hugo/resources/page" 26 27 "github.com/gohugoio/hugo/hugofs" 28 29 "github.com/gohugoio/hugo/helpers" 30 31 "github.com/gohugoio/hugo/parser" 32 "github.com/gohugoio/hugo/parser/metadecoders" 33 34 "github.com/gohugoio/hugo/hugolib" 35 36 "github.com/spf13/cobra" 37 ) 38 39 var _ cmder = (*convertCmd)(nil) 40 41 type convertCmd struct { 42 outputDir string 43 unsafe bool 44 45 *baseBuilderCmd 46 } 47 48 func (b *commandsBuilder) newConvertCmd() *convertCmd { 49 cc := &convertCmd{} 50 51 cmd := &cobra.Command{ 52 Use: "convert", 53 Short: "Convert your content to different formats", 54 Long: `Convert your content (e.g. front matter) to different formats. 55 56 See convert's subcommands toJSON, toTOML and toYAML for more information.`, 57 RunE: nil, 58 } 59 60 cmd.AddCommand( 61 &cobra.Command{ 62 Use: "toJSON", 63 Short: "Convert front matter to JSON", 64 Long: `toJSON converts all front matter in the content directory 65 to use JSON for the front matter.`, 66 RunE: func(cmd *cobra.Command, args []string) error { 67 return cc.convertContents(metadecoders.JSON) 68 }, 69 }, 70 &cobra.Command{ 71 Use: "toTOML", 72 Short: "Convert front matter to TOML", 73 Long: `toTOML converts all front matter in the content directory 74 to use TOML for the front matter.`, 75 RunE: func(cmd *cobra.Command, args []string) error { 76 return cc.convertContents(metadecoders.TOML) 77 }, 78 }, 79 &cobra.Command{ 80 Use: "toYAML", 81 Short: "Convert front matter to YAML", 82 Long: `toYAML converts all front matter in the content directory 83 to use YAML for the front matter.`, 84 RunE: func(cmd *cobra.Command, args []string) error { 85 return cc.convertContents(metadecoders.YAML) 86 }, 87 }, 88 ) 89 90 cmd.PersistentFlags().StringVarP(&cc.outputDir, "output", "o", "", "filesystem path to write files to") 91 cmd.PersistentFlags().BoolVar(&cc.unsafe, "unsafe", false, "enable less safe operations, please backup first") 92 93 cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd) 94 95 return cc 96 } 97 98 func (cc *convertCmd) convertContents(format metadecoders.Format) error { 99 if cc.outputDir == "" && !cc.unsafe { 100 return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path") 101 } 102 103 c, err := initializeConfig(true, false, false, &cc.hugoBuilderCommon, cc, nil) 104 if err != nil { 105 return err 106 } 107 108 c.Cfg.Set("buildDrafts", true) 109 110 h, err := hugolib.NewHugoSites(*c.DepsCfg) 111 if err != nil { 112 return err 113 } 114 115 if err := h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { 116 return err 117 } 118 119 site := h.Sites[0] 120 121 site.Log.Println("processing", len(site.AllPages()), "content files") 122 for _, p := range site.AllPages() { 123 if err := cc.convertAndSavePage(p, site, format); err != nil { 124 return err 125 } 126 } 127 return nil 128 } 129 130 func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error { 131 // The resources are not in .Site.AllPages. 132 for _, r := range p.Resources().ByType("page") { 133 if err := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil { 134 return err 135 } 136 } 137 138 if p.File().IsZero() { 139 // No content file. 140 return nil 141 } 142 143 errMsg := fmt.Errorf("Error processing file %q", p.File().Path()) 144 145 site.Log.Infoln("Attempting to convert", p.File().Filename()) 146 147 f := p.File() 148 file, err := f.FileInfo().Meta().Open() 149 if err != nil { 150 site.Log.Errorln(errMsg) 151 file.Close() 152 return nil 153 } 154 155 pf, err := pageparser.ParseFrontMatterAndContent(file) 156 if err != nil { 157 site.Log.Errorln(errMsg) 158 file.Close() 159 return err 160 } 161 162 file.Close() 163 164 // better handling of dates in formats that don't have support for them 165 if pf.FrontMatterFormat == metadecoders.JSON || pf.FrontMatterFormat == metadecoders.YAML || pf.FrontMatterFormat == metadecoders.TOML { 166 for k, v := range pf.FrontMatter { 167 switch vv := v.(type) { 168 case time.Time: 169 pf.FrontMatter[k] = vv.Format(time.RFC3339) 170 } 171 } 172 } 173 174 var newContent bytes.Buffer 175 err = parser.InterfaceToFrontMatter(pf.FrontMatter, targetFormat, &newContent) 176 if err != nil { 177 site.Log.Errorln(errMsg) 178 return err 179 } 180 181 newContent.Write(pf.Content) 182 183 newFilename := p.File().Filename() 184 185 if cc.outputDir != "" { 186 contentDir := strings.TrimSuffix(newFilename, p.File().Path()) 187 contentDir = filepath.Base(contentDir) 188 189 newFilename = filepath.Join(cc.outputDir, contentDir, p.File().Path()) 190 } 191 192 fs := hugofs.Os 193 if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil { 194 return fmt.Errorf("Failed to save file %q:: %w", newFilename, err) 195 } 196 197 return nil 198 } 199 200 type parsedFile struct { 201 frontMatterFormat metadecoders.Format 202 frontMatterSource []byte 203 frontMatter map[string]any 204 205 // Everything after Front Matter 206 content []byte 207 }