packagerendererimport("bytes""fmt""html/template""io""io/fs""os""path""path/filepath""static-repo/internal/utils""strings""sync"chromahtml"github.com/alecthomas/chroma/v2/formatters/html""github.com/alecthomas/chroma/v2/lexers""github.com/alecthomas/chroma/v2/styles""github.com/yuin/goldmark"highlighting"github.com/yuin/goldmark-highlighting/v2"meta"github.com/yuin/goldmark-meta""github.com/yuin/goldmark/extension""github.com/yuin/goldmark/parser""github.com/yuin/goldmark/renderer/html"alertcallouts"github.com/zmtcreative/gm-alert-callouts")typeRendererstruct{Markdowngoldmark.Markdowntemplates*template.TemplatebufPoolsync.Pool}funcNewRenderer(templatesFSfs.FS)(*Renderer,error){md:=goldmark.New(goldmark.WithExtensions(highlighting.NewHighlighting(highlighting.WithFormatOptions(chromahtml.WithClasses(true),chromahtml.TabWidth(4),),),alertcallouts.NewAlertCallouts(alertcallouts.UseGFMStrictIcons()),extension.GFM,meta.Meta,NewLinkExtension(),),goldmark.WithRendererOptions(html.WithUnsafe(),),)tmpl:=template.New("renderer")_,err:=tmpl.ParseFS(templatesFS,"templates/partials/markdown.html","templates/partials/code.html","templates/partials/pdf.html","templates/partials/download.html",)iferr!=nil{returnnil,fmt.Errorf("failed to parse renderer templates: %v",err)}return&Renderer{Markdown:md,templates:tmpl,bufPool:sync.Pool{New:func()interface{}{returnnew(bytes.Buffer)},},},nil}func(r*Renderer)RenderFile(filePath,contentDir,outputDir,urlPrefixstring)(string,error){ctype:=utils.GetContentType(filePath)fmt.Printf("Rendering %s: %s\n",ctype,filePath)ifctype=="text"||ctype=="markdown"{info,err:=os.Stat(filePath)iferr==nil&&info.Size()>1024*1024{returnr.RenderDownload(filePath,contentDir,outputDir,urlPrefix,"This file is too large to render.")}}ifctype=="markdown"{returnr.RenderMarkdown(filePath)}ifctype=="image"{returnr.RenderMedia(filePath,"img",contentDir,outputDir,urlPrefix)}ifctype=="video"{returnr.RenderMedia(filePath,"video",contentDir,outputDir,urlPrefix)}ifctype=="pdf"{returnr.RenderPDF(filePath,contentDir,outputDir,urlPrefix)}ifctype=="text"{returnr.HighlightCode(filePath)}ifctype=="binary"||ctype=="archive"{returnr.RenderDownload(filePath,contentDir,outputDir,urlPrefix,"")}return"",fmt.Errorf("unsupported file type: %s",ctype)}func(r*Renderer)RenderPDF(filePath,contentDir,outputDir,urlPrefixstring)(string,error){relPath,err:=filepath.Rel(contentDir,filePath)iferr!=nil{return"",fmt.Errorf("failed to get relative path for %s: %v",filePath,err)}destPath:=filepath.Join(outputDir,relPath)iferr:=os.MkdirAll(filepath.Dir(destPath),0755);err!=nil{return"",err}iferr:=utils.CopyFile(filePath,destPath);err!=nil{return"",err}buf:=r.bufPool.Get().(*bytes.Buffer)buf.Reset()deferr.bufPool.Put(buf)// Use path.Join for URLs to ensure forward slashesurlPath:=path.Join(urlPrefix,filepath.ToSlash(relPath))if!strings.HasPrefix(urlPath,"/"){urlPath="/"+urlPath}data:=struct{Pathstring}{Path:urlPath,}iferr:=r.templates.ExecuteTemplate(buf,"pdf.html",data);err!=nil{return"",err}returnbuf.String(),nil}func(r*Renderer)RenderDownload(filePath,contentDir,outputDir,urlPrefix,messagestring)(string,error){relPath,err:=filepath.Rel(contentDir,filePath)iferr!=nil{return"",fmt.Errorf("failed to get relative path for %s: %v",filePath,err)}destPath:=filepath.Join(outputDir,relPath)iferr:=os.MkdirAll(filepath.Dir(destPath),0755);err!=nil{return"",err}iferr:=utils.CopyFile(filePath,destPath);err!=nil{return"",err}// Use path.Join for URLs to ensure forward slashesurlPath:=path.Join(urlPrefix,filepath.ToSlash(relPath))if!strings.HasPrefix(urlPath,"/"){urlPath="/"+urlPath}buf:=r.bufPool.Get().(*bytes.Buffer)buf.Reset()deferr.bufPool.Put(buf)data:=struct{PathstringNamestringMessagestring}{Path:urlPath,Name:filepath.Base(filePath),Message:message,}iferr:=r.templates.ExecuteTemplate(buf,"download.html",data);err!=nil{return"",err}returnbuf.String(),nil}// RenderMarkdown renders a Markdown file and returns the HTML contentfunc(r*Renderer)RenderMarkdown(filePathstring)(string,error){input,err:=os.ReadFile(filePath)iferr!=nil{return"",err}contentBuf:=r.bufPool.Get().(*bytes.Buffer)contentBuf.Reset()deferr.bufPool.Put(contentBuf)context:=parser.NewContext()iferr:=r.Markdown.Convert(input,contentBuf,parser.WithContext(context));err!=nil{return"",err}outBuf:=r.bufPool.Get().(*bytes.Buffer)outBuf.Reset()deferr.bufPool.Put(outBuf)data:=struct{Contenttemplate.HTML}{Content:template.HTML(contentBuf.String()),}iferr:=r.templates.ExecuteTemplate(outBuf,"markdown.html",data);err!=nil{return"",err}returnoutBuf.String(),nil}func(r*Renderer)ExtractFrontmatter(filePathstring)(map[string]interface{},error){input,err:=os.ReadFile(filePath)iferr!=nil{returnnil,err}context:=parser.NewContext()// We only need to parse, not render, but goldmark doesn't have a simple "Parse" that populates meta// without some extra work. Using Convert with io.Discard is recommended for side effects.iferr:=r.Markdown.Convert(input,io.Discard,parser.WithContext(context));err!=nil{returnnil,err}returnmeta.Get(context),nil}func(r*Renderer)HighlightCode(filePathstring)(string,error){content,err:=os.ReadFile(filePath)iferr!=nil{return"",err}lexer:=lexers.Get(filePath)iflexer==nil{lexer=lexers.Analyse(string(content))}iflexer==nil{lexer=lexers.Fallback}style:=styles.Get("monokai")formatter:=chromahtml.New(chromahtml.WithClasses(true))iterator,err:=lexer.Tokenise(nil,string(content))iferr!=nil{return"",err}buf:=r.bufPool.Get().(*bytes.Buffer)buf.Reset()deferr.bufPool.Put(buf)err=formatter.Format(buf,style,iterator)iferr!=nil{return"",err}outBuf:=r.bufPool.Get().(*bytes.Buffer)outBuf.Reset()deferr.bufPool.Put(outBuf)data:=struct{Contenttemplate.HTML}{Content:template.HTML(buf.String()),}iferr:=r.templates.ExecuteTemplate(outBuf,"code.html",data);err!=nil{return"",err}returnoutBuf.String(),nil}func(r*Renderer)RenderMedia(filePath,tag,contentDir,outputDir,urlPrefixstring)(string,error){relPath,err:=filepath.Rel(contentDir,filePath)iferr!=nil{return"",fmt.Errorf("failed to get relative path for %s: %v",filePath,err)}// Copy media file to output dirdestPath:=filepath.Join(outputDir,relPath)iferr:=os.MkdirAll(filepath.Dir(destPath),0755);err!=nil{return"",err}iferr:=utils.CopyFile(filePath,destPath);err!=nil{return"",err}// Use path.Join for URLs to ensure forward slashesurlPath:=path.Join(urlPrefix,filepath.ToSlash(relPath))if!strings.HasPrefix(urlPath,"/"){urlPath="/"+urlPath}iftag=="img"{returnfmt.Sprintf(`<img src="%s" alt="%s">`,urlPath,filepath.Base(filePath)),nil}returnfmt.Sprintf(`<video controls src="%s"></video>`,urlPath),nil}