inline_imports.go - hugo - [fork] hugo port for 9front
HTML git clone https://git.drkhsh.at/hugo.git
DIR Log
DIR Files
DIR Refs
DIR Submodules
DIR README
DIR LICENSE
---
inline_imports.go (5905B)
---
1 // Copyright 2024 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 cssjs
15
16 import (
17 "crypto/sha256"
18 "encoding/hex"
19 "errors"
20 "fmt"
21 "io"
22 "path"
23 "path/filepath"
24 "regexp"
25 "strconv"
26 "strings"
27
28 "github.com/gohugoio/hugo/common/herrors"
29 "github.com/gohugoio/hugo/common/loggers"
30 "github.com/gohugoio/hugo/common/text"
31 "github.com/gohugoio/hugo/hugofs"
32 "github.com/gohugoio/hugo/identity"
33 "github.com/spf13/afero"
34 )
35
36 const importIdentifier = "@import"
37
38 var (
39 cssSyntaxErrorRe = regexp.MustCompile(`> (\d+) \|`)
40 shouldImportRe = regexp.MustCompile(`^@import ["'](.*?)["'];?\s*(/\*.*\*/)?$`)
41 )
42
43 type fileOffset struct {
44 Filename string
45 Offset int
46 }
47
48 type importResolver struct {
49 r io.Reader
50 inPath string
51 opts InlineImports
52
53 contentSeen map[string]bool
54 dependencyManager identity.Manager
55 linemap map[int]fileOffset
56 fs afero.Fs
57 logger loggers.Logger
58 }
59
60 func newImportResolver(r io.Reader, inPath string, opts InlineImports, fs afero.Fs, logger loggers.Logger, dependencyManager identity.Manager) *importResolver {
61 return &importResolver{
62 r: r,
63 dependencyManager: dependencyManager,
64 inPath: inPath,
65 fs: fs, logger: logger,
66 linemap: make(map[int]fileOffset), contentSeen: make(map[string]bool),
67 opts: opts,
68 }
69 }
70
71 func (imp *importResolver) contentHash(filename string) ([]byte, string) {
72 b, err := afero.ReadFile(imp.fs, filename)
73 if err != nil {
74 return nil, ""
75 }
76 h := sha256.New()
77 h.Write(b)
78 return b, hex.EncodeToString(h.Sum(nil))
79 }
80
81 func (imp *importResolver) importRecursive(
82 lineNum int,
83 content string,
84 inPath string,
85 ) (int, string, error) {
86 basePath := path.Dir(inPath)
87
88 var replacements []string
89 lines := strings.Split(content, "\n")
90
91 trackLine := func(i, offset int, line string) {
92 // TODO(bep) this is not very efficient.
93 imp.linemap[i+lineNum] = fileOffset{Filename: inPath, Offset: offset}
94 }
95
96 i := 0
97 for offset, line := range lines {
98 i++
99 lineTrimmed := strings.TrimSpace(line)
100 column := strings.Index(line, lineTrimmed)
101 line = lineTrimmed
102
103 if !imp.shouldImport(line) {
104 trackLine(i, offset, line)
105 } else {
106 path := strings.Trim(strings.TrimPrefix(line, importIdentifier), " \"';")
107 filename := filepath.Join(basePath, path)
108 imp.dependencyManager.AddIdentity(identity.CleanStringIdentity(filename))
109 importContent, hash := imp.contentHash(filename)
110
111 if importContent == nil {
112 if imp.opts.SkipInlineImportsNotFound {
113 trackLine(i, offset, line)
114 continue
115 }
116 pos := text.Position{
117 Filename: inPath,
118 LineNumber: offset + 1,
119 ColumnNumber: column + 1,
120 }
121 return 0, "", herrors.NewFileErrorFromFileInPos(fmt.Errorf("failed to resolve CSS @import \"%s\"", filename), pos, imp.fs, nil)
122 }
123
124 i--
125
126 if imp.contentSeen[hash] {
127 i++
128 // Just replace the line with an empty string.
129 replacements = append(replacements, []string{line, ""}...)
130 trackLine(i, offset, "IMPORT")
131 continue
132 }
133
134 imp.contentSeen[hash] = true
135
136 // Handle recursive imports.
137 l, nested, err := imp.importRecursive(i+lineNum, string(importContent), filepath.ToSlash(filename))
138 if err != nil {
139 return 0, "", err
140 }
141
142 trackLine(i, offset, line)
143
144 i += l
145
146 importContent = []byte(nested)
147
148 replacements = append(replacements, []string{line, string(importContent)}...)
149 }
150 }
151
152 if len(replacements) > 0 {
153 repl := strings.NewReplacer(replacements...)
154 content = repl.Replace(content)
155 }
156
157 return i, content, nil
158 }
159
160 func (imp *importResolver) resolve() (io.Reader, error) {
161 content, err := io.ReadAll(imp.r)
162 if err != nil {
163 return nil, err
164 }
165
166 contents := string(content)
167
168 _, newContent, err := imp.importRecursive(0, contents, imp.inPath)
169 if err != nil {
170 return nil, err
171 }
172
173 return strings.NewReader(newContent), nil
174 }
175
176 // See https://www.w3schools.com/cssref/pr_import_rule.asp
177 // We currently only support simple file imports, no urls, no media queries.
178 // So this is OK:
179 //
180 // @import "navigation.css";
181 //
182 // This is not:
183 //
184 // @import url("navigation.css");
185 // @import "mobstyle.css" screen and (max-width: 768px);
186 func (imp *importResolver) shouldImport(s string) bool {
187 if !strings.HasPrefix(s, importIdentifier) {
188 return false
189 }
190 if strings.Contains(s, "url(") {
191 return false
192 }
193
194 m := shouldImportRe.FindStringSubmatch(s)
195 if m == nil {
196 return false
197 }
198
199 if len(m) != 3 {
200 return false
201 }
202
203 if tailwindImportExclude(m[1]) {
204 return false
205 }
206
207 return true
208 }
209
210 func (imp *importResolver) toFileError(output string) error {
211 inErr := errors.New(output)
212
213 match := cssSyntaxErrorRe.FindStringSubmatch(output)
214 if match == nil {
215 return inErr
216 }
217
218 lineNum, err := strconv.Atoi(match[1])
219 if err != nil {
220 return inErr
221 }
222
223 file, ok := imp.linemap[lineNum]
224 if !ok {
225 return inErr
226 }
227
228 fi, err := imp.fs.Stat(file.Filename)
229 if err != nil {
230 return inErr
231 }
232
233 meta := fi.(hugofs.FileMetaInfo).Meta()
234 realFilename := meta.Filename
235 f, err := meta.Open()
236 if err != nil {
237 return inErr
238 }
239 defer f.Close()
240
241 ferr := herrors.NewFileErrorFromName(inErr, realFilename)
242 pos := ferr.Position()
243 pos.LineNumber = file.Offset + 1
244 return ferr.UpdatePosition(pos).UpdateContent(f, nil)
245
246 // return herrors.NewFileErrorFromFile(inErr, file.Filename, realFilename, hugofs.Os, herrors.SimpleLineMatcher)
247 }