master
  1package main
  2
  3import (
  4	"bytes"
  5	"context"
  6	"fmt"
  7	"html/template"
  8	"os"
  9	"path/filepath"
 10	"runtime"
 11	"strings"
 12	"sync"
 13
 14	"github.com/alecthomas/chroma/v2/formatters/html"
 15	"github.com/alecthomas/chroma/v2/lexers"
 16	"github.com/alecthomas/chroma/v2/styles"
 17
 18	"github.com/antonmedv/gitmal/pkg/git"
 19	"github.com/antonmedv/gitmal/pkg/links"
 20	"github.com/antonmedv/gitmal/pkg/progress_bar"
 21	"github.com/antonmedv/gitmal/pkg/templates"
 22)
 23
 24func generateBlobs(files []git.Blob, params Params) error {
 25	// Prepare shared, read-only resources
 26	var css strings.Builder
 27	style := styles.Get(params.Style)
 28	if style == nil {
 29		return fmt.Errorf("unknown style: %s", params.Style)
 30	}
 31
 32	formatterOptions := []html.Option{
 33		html.WithLineNumbers(true),
 34		html.WithLinkableLineNumbers(true, "L"),
 35		html.WithClasses(true),
 36		html.WithCSSComments(false),
 37	}
 38
 39	// Use a temporary formatter to render CSS once
 40	if err := html.New(formatterOptions...).WriteCSS(&css, style); err != nil {
 41		return err
 42	}
 43
 44	dirsSet := links.BuildDirSet(files)
 45	filesSet := links.BuildFileSet(files)
 46
 47	// Bounded worker pool
 48	workers := runtime.NumCPU()
 49	if workers < 1 {
 50		workers = 1
 51	}
 52
 53	ctx, cancel := context.WithCancel(context.Background())
 54	defer cancel()
 55
 56	jobs := make(chan git.Blob)
 57	errCh := make(chan error, 1)
 58	var wg sync.WaitGroup
 59
 60	p := progress_bar.NewProgressBar("blobs for "+params.Ref.String(), len(files))
 61
 62	workerFn := func() {
 63		defer wg.Done()
 64
 65		// Per-worker instances
 66		md := createMarkdown(params.Style)
 67		formatter := html.New(formatterOptions...)
 68
 69		check := func(err error) bool {
 70			if err != nil {
 71				select {
 72				case errCh <- err:
 73					cancel()
 74				default:
 75				}
 76				return true
 77			}
 78			return false
 79		}
 80
 81		for {
 82			select {
 83			case <-ctx.Done():
 84				return
 85			case blob, ok := <-jobs:
 86				if !ok {
 87					return
 88				}
 89				func() {
 90					var content string
 91					data, isBin, err := git.BlobContent(params.Ref, blob.Path, params.RepoDir)
 92					if check(err) {
 93						return
 94					}
 95
 96					isImg := isImage(blob.Path)
 97					if !isBin {
 98						content = string(data)
 99					}
100
101					outPath := filepath.Join(params.OutputDir, "blob", params.Ref.DirName(), blob.Path) + ".html"
102					if err := os.MkdirAll(filepath.Dir(outPath), 0o755); check(err) {
103						return
104					}
105
106					f, err := os.Create(outPath)
107					if check(err) {
108						return
109					}
110					defer func() {
111						_ = f.Close()
112					}()
113
114					depth := 0
115					if strings.Contains(blob.Path, "/") {
116						depth = len(strings.Split(blob.Path, "/")) - 1
117					}
118					rootHref := strings.Repeat("../", depth+2)
119
120					rawPath := filepath.Join(params.OutputDir, "raw", params.Ref.DirName(), blob.Path)
121					if err := os.MkdirAll(filepath.Dir(rawPath), 0o755); check(err) {
122						return
123					}
124
125					rf, err := os.Create(rawPath)
126					if check(err) {
127						return
128					}
129
130					if _, err := rf.Write(data); check(err) {
131						return
132					}
133					_ = rf.Close()
134
135					rawHref := filepath.Join(rootHref, "raw", params.Ref.DirName(), blob.Path)
136
137					if isMarkdown(blob.Path) {
138						var b bytes.Buffer
139						if err := md.Convert([]byte(content), &b); check(err) {
140							return
141						}
142
143						contentHTML := links.Resolve(
144							b.String(),
145							blob.Path,
146							rootHref,
147							params.Ref.DirName(),
148							dirsSet,
149							filesSet,
150						)
151
152						err = templates.MarkdownTemplate.ExecuteTemplate(f, "layout.gohtml", templates.MarkdownParams{
153							LayoutParams: templates.LayoutParams{
154								Title:         fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
155								Dark:          params.Dark,
156								CSSMarkdown:   cssMarkdown(params.Dark),
157								Name:          params.Name,
158								SiteName:      params.SiteName,
159								RootHref:      rootHref + params.RootPrefix,
160								RepoHref:      rootHref,
161								CurrentRefDir: params.Ref.DirName(),
162								Selected:      "code",
163								InlineStyles:  params.InlineStyles,
164							},
165							HeaderParams: templates.HeaderParams{
166								Ref:         params.Ref,
167								Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
168								RawHref:     rawHref,
169							},
170							Blob:    blob,
171							Content: template.HTML(contentHTML),
172						})
173						if check(err) {
174							return
175						}
176
177					} else {
178
179						var contentHTML template.HTML
180						if !isBin {
181							var b bytes.Buffer
182							lx := lexers.Match(blob.Path)
183							if lx == nil {
184								lx = lexers.Fallback
185							}
186							iterator, _ := lx.Tokenise(nil, content)
187							if err := formatter.Format(&b, style, iterator); check(err) {
188								return
189							}
190							contentHTML = template.HTML(b.String())
191
192						}
193
194						if isImg {
195							contentHTML = template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" />`, rawHref, blob.FileName))
196						}
197
198						err = templates.BlobTemplate.ExecuteTemplate(f, "layout.gohtml", templates.BlobParams{
199							LayoutParams: templates.LayoutParams{
200								Title:         fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
201								Dark:          params.Dark,
202								Name:          params.Name,
203								SiteName:      params.SiteName,
204								RootHref:      rootHref + params.RootPrefix,
205								RepoHref:      rootHref,
206								CurrentRefDir: params.Ref.DirName(),
207								Selected:      "code",
208								InlineStyles:  params.InlineStyles,
209							},
210							HeaderParams: templates.HeaderParams{
211								Ref:         params.Ref,
212								Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
213								RawHref:     rawHref,
214							},
215							CSS:      template.CSS(css.String()),
216							Blob:     blob,
217							IsBinary: isBin,
218							IsImage:  isImg,
219							Content:  contentHTML,
220						})
221						if check(err) {
222							return
223						}
224					}
225				}()
226
227				p.Inc()
228			}
229		}
230	}
231
232	// Start workers
233	wg.Add(workers)
234	for i := 0; i < workers; i++ {
235		go workerFn()
236	}
237
238	// Feed jobs
239	go func() {
240		defer close(jobs)
241		for _, b := range files {
242			select {
243			case <-ctx.Done():
244				return
245			case jobs <- b:
246			}
247		}
248	}()
249
250	// Wait for workers
251	doneCh := make(chan struct{})
252	go func() {
253		wg.Wait()
254		close(doneCh)
255	}()
256
257	var runErr error
258	select {
259	case runErr = <-errCh:
260		// error occurred, wait workers to finish
261		<-doneCh
262	case <-doneCh:
263	}
264
265	p.Done()
266	return runErr
267}