package site import ( "bytes" "context" "fmt" "os" "path" "github.com/a-h/templ" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/content" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/stage" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/web/templates" ) // buildFragments renders every stage fragment once, in manifest order: // // 1. every document's own fragment, L1 first in area order // 2. every artefact fragment, in the same document order // // The order matters: the first len(content.Areas) entries are the six area // landing fragments, which is the default stream, and the server reads that // order out of the manifest without loading the content tree. The panel is a // content sink, so this is the only place that decides what a fragment is. // Neither driver renders anything itself. func (s *Site) buildFragments() error { order := make([]*content.Document, 0, len(s.Content.Docs)) for _, a := range content.Areas { for _, d := range s.PublicInArea(a.Key) { if d.Level == 1 { order = append(order, d) } } } rest := make([]*content.Document, 0, len(s.Content.Docs)) for _, a := range content.Areas { for _, d := range s.PublicInArea(a.Key) { if d.Level != 1 { rest = append(rest, d) } } } order = append(order, rest...) for _, d := range order { if err := s.addFragment(s.docFragment(d)); err != nil { return err } } for _, d := range order { for i, a := range d.Artefacts { f, err := s.artefactFragment(d, a, stage.ArtefactFragmentID(d.ID, i)) if err != nil { return err } if err := s.addFragment(f); err != nil { return err } } } return nil } func (s *Site) addFragment(f *Fragment) error { if _, dup := s.fragByID[f.ID]; dup { return fmt.Errorf("stage fragment id %q is not unique", f.ID) } html, err := renderFragment(f.component()) if err != nil { return fmt.Errorf("render stage fragment %s: %w", f.ID, err) } f.HTML = html s.Fragments = append(s.Fragments, f) s.fragByID[f.ID] = f s.FragmentsByDoc[f.DocID] = append(s.FragmentsByDoc[f.DocID], f.ID) s.Manifest.Entries = append(s.Manifest.Entries, stage.Entry{ ID: f.ID, DocID: f.DocID, Title: f.Title, Kind: f.Kind, File: f.File, }) return nil } // fragment builds the stage value this fragment renders from. func (f *Fragment) component() stage.Fragment { out := stage.Fragment{ ID: f.ID, Kind: f.Kind, Title: f.Title, } out.Subtitle = f.subtitle out.Body = f.body out.Code = f.code out.Src = f.src out.Alt = f.alt out.Links = f.links out.Confidence = f.confidence out.DocURL = f.docURL out.DocTitle = f.docTitle return out } func renderFragment(f stage.Fragment) (string, error) { var buf bytes.Buffer if err := templates.FragmentView(f).Render(context.Background(), &buf); err != nil { return "", err } return buf.String(), nil } // docFragment is the document's own body, shown in the panel when a reader // asks for the page itself rather than one of its artefacts. func (s *Site) docFragment(d *content.Document) *Fragment { area, _ := content.AreaByKey(d.Area) return &Fragment{ ID: stage.DocFragmentID(d.ID), DocID: d.ID, Kind: stage.KindText, Title: d.Title, subtitle: area.Title + " ยท " + templates.LevelLabel(d.Level), body: templ.Raw(d.HTML), confidence: d.Confidence, docURL: d.URL, docTitle: "Read the page", File: "stage/" + stage.DocFragmentID(d.ID) + ".html", } } // artefactFragment renders one artefact into the kind the panel understands. func (s *Site) artefactFragment(d *content.Document, a content.Artefact, id string) (*Fragment, error) { ra, err := s.Content.Resolve(d, a) if err != nil { return nil, err } f := &Fragment{ ID: id, DocID: d.ID, Kind: ra.Kind, Title: a.Label, ArtefactURL: a.URL, confidence: d.Confidence, File: "stage/" + id + ".html", } if ra.Target != nil { f.subtitle = ra.Target.Title f.body = templ.Raw(ra.Target.HTML) f.confidence = ra.Target.Confidence f.docURL = ra.Target.URL f.docTitle = "Open " + ra.Target.Title return f, nil } f.subtitle = ra.Rel switch ra.Kind { case stage.KindCode: src, err := os.ReadFile(ra.File) if err != nil { return nil, fmt.Errorf("%s: read code artefact: %w", d.Path, err) } var buf bytes.Buffer lang := content.LanguageFor(ra.File) if err := s.Renderer.Highlight(&buf, lang, src); err != nil { return nil, fmt.Errorf("%s: highlight artefact: %w", d.Path, err) } f.code = &stage.Code{Language: lang, Label: path.Base(ra.Rel), HTML: templ.Raw(buf.String())} case stage.KindImage, stage.KindDiagram, stage.KindVideo: f.src = ra.Href f.alt = a.Label default: f.links = []stage.Link{{Label: a.Label, Href: ra.Href, Note: ra.Rel}} } return f, nil } // fragForEntry looks up a build-time fragment by manifest entry. func (s *Site) fragForEntry(e stage.Entry) (*Fragment, bool) { f, ok := s.fragByID[e.ID] return f, ok } // FragmentHTML returns the pre-rendered markup for a fragment id. Both drivers // serve these bytes: the route driver as a file, the stream driver as an event. func (s *Site) FragmentHTML(id string) (string, bool) { f, ok := s.fragByID[id] if !ok { return "", false } return f.HTML, true } // FragmentsByID is a copy of the fragment index, for tests and the manifest. func (s *Site) FragmentsByID() map[string]*Fragment { out := make(map[string]*Fragment, len(s.fragByID)) for k, v := range s.fragByID { out[k] = v } return out }