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 }