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 }