classifier.go (5042B)
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 files
15
16 import (
17 "bufio"
18 "fmt"
19 "io"
20 "os"
21 "path/filepath"
22 "sort"
23 "strings"
24 "unicode"
25
26 "github.com/spf13/afero"
27 )
28
29 const (
30 // The NPM package.json "template" file.
31 FilenamePackageHugoJSON = "package.hugo.json"
32 // The NPM package file.
33 FilenamePackageJSON = "package.json"
34 )
35
36 var (
37 // This should be the only list of valid extensions for content files.
38 contentFileExtensions = []string{
39 "html", "htm",
40 "mdown", "markdown", "md",
41 "asciidoc", "adoc", "ad",
42 "rest", "rst",
43 "org",
44 "pandoc", "pdc",
45 }
46
47 contentFileExtensionsSet map[string]bool
48
49 htmlFileExtensions = []string{
50 "html", "htm",
51 }
52
53 htmlFileExtensionsSet map[string]bool
54 )
55
56 func init() {
57 contentFileExtensionsSet = make(map[string]bool)
58 for _, ext := range contentFileExtensions {
59 contentFileExtensionsSet[ext] = true
60 }
61 htmlFileExtensionsSet = make(map[string]bool)
62 for _, ext := range htmlFileExtensions {
63 htmlFileExtensionsSet[ext] = true
64 }
65 }
66
67 func IsContentFile(filename string) bool {
68 return contentFileExtensionsSet[strings.TrimPrefix(filepath.Ext(filename), ".")]
69 }
70
71 func IsIndexContentFile(filename string) bool {
72 if !IsContentFile(filename) {
73 return false
74 }
75
76 base := filepath.Base(filename)
77
78 return strings.HasPrefix(base, "index.") || strings.HasPrefix(base, "_index.")
79 }
80
81 func IsHTMLFile(filename string) bool {
82 return htmlFileExtensionsSet[strings.TrimPrefix(filepath.Ext(filename), ".")]
83 }
84
85 func IsContentExt(ext string) bool {
86 return contentFileExtensionsSet[ext]
87 }
88
89 type ContentClass string
90
91 const (
92 ContentClassLeaf ContentClass = "leaf"
93 ContentClassBranch ContentClass = "branch"
94 ContentClassFile ContentClass = "zfile" // Sort below
95 ContentClassContent ContentClass = "zcontent"
96 )
97
98 func (c ContentClass) IsBundle() bool {
99 return c == ContentClassLeaf || c == ContentClassBranch
100 }
101
102 func ClassifyContentFile(filename string, open func() (afero.File, error)) ContentClass {
103 if !IsContentFile(filename) {
104 return ContentClassFile
105 }
106
107 if IsHTMLFile(filename) {
108 // We need to look inside the file. If the first non-whitespace
109 // character is a "<", then we treat it as a regular file.
110 // Eearlier we created pages for these files, but that had all sorts
111 // of troubles, and isn't what it says in the documentation.
112 // See https://github.com/gohugoio/hugo/issues/7030
113 if open == nil {
114 panic(fmt.Sprintf("no file opener provided for %q", filename))
115 }
116
117 f, err := open()
118 if err != nil {
119 return ContentClassFile
120 }
121 ishtml := isHTMLContent(f)
122 f.Close()
123 if ishtml {
124 return ContentClassFile
125 }
126
127 }
128
129 if strings.HasPrefix(filename, "_index.") {
130 return ContentClassBranch
131 }
132
133 if strings.HasPrefix(filename, "index.") {
134 return ContentClassLeaf
135 }
136
137 return ContentClassContent
138 }
139
140 var htmlComment = []rune{'<', '!', '-', '-'}
141
142 func isHTMLContent(r io.Reader) bool {
143 br := bufio.NewReader(r)
144 i := 0
145 for {
146 c, _, err := br.ReadRune()
147 if err != nil {
148 break
149 }
150
151 if i > 0 {
152 if i >= len(htmlComment) {
153 return false
154 }
155
156 if c != htmlComment[i] {
157 return true
158 }
159
160 i++
161 continue
162 }
163
164 if !unicode.IsSpace(c) {
165 if i == 0 && c != '<' {
166 return false
167 }
168 i++
169 }
170 }
171 return true
172 }
173
174 const (
175 ComponentFolderArchetypes = "archetypes"
176 ComponentFolderStatic = "static"
177 ComponentFolderLayouts = "layouts"
178 ComponentFolderContent = "content"
179 ComponentFolderData = "data"
180 ComponentFolderAssets = "assets"
181 ComponentFolderI18n = "i18n"
182
183 FolderResources = "resources"
184 FolderJSConfig = "_jsconfig" // Mounted below /assets with postcss.config.js etc.
185 )
186
187 var (
188 JsConfigFolderMountPrefix = filepath.Join(ComponentFolderAssets, FolderJSConfig)
189
190 ComponentFolders = []string{
191 ComponentFolderArchetypes,
192 ComponentFolderStatic,
193 ComponentFolderLayouts,
194 ComponentFolderContent,
195 ComponentFolderData,
196 ComponentFolderAssets,
197 ComponentFolderI18n,
198 }
199
200 componentFoldersSet = make(map[string]bool)
201 )
202
203 func init() {
204 sort.Strings(ComponentFolders)
205 for _, f := range ComponentFolders {
206 componentFoldersSet[f] = true
207 }
208 }
209
210 // ResolveComponentFolder returns "content" from "content/blog/foo.md" etc.
211 func ResolveComponentFolder(filename string) string {
212 filename = strings.TrimPrefix(filename, string(os.PathSeparator))
213 for _, cf := range ComponentFolders {
214 if strings.HasPrefix(filename, cf) {
215 return cf
216 }
217 }
218
219 return ""
220 }
221
222 func IsComponentFolder(name string) bool {
223 return componentFoldersSet[name]
224 }