URI: 
       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 }