add the gallery shortcode and the seam it needed
A feature now learns which bundle is rendering: render.Bundle puts an Origin —
the bundle's directory plus the rooted fs.FS — on the parse context, and
render.OriginFrom reads it back. Available while parsing, not while rendering,
which decides where a feature does its filesystem work: goldmark hands the
context to a block parser and not to a node renderer, so gallery gathers its
filenames at parse time and carries them on the node.
Reads stay inside the site root because Origin passes the fs.FS rather than a
path to join (ADR-0031).
Fragment{Args, Items} lands with it (ADR-0037), so figure's template now reads
.Args.src. Authored arguments and engine-gathered items stay in separate fields:
a src argument beside a src the engine found would otherwise silently pick one.
A gallery is pictures beside the bundle, in filename order, skipping
subdirectories and anything a browser cannot show. Filename order is what makes
the sparse numeric-prefix convention work without numbers in URLs (ADR-0016).
New latent row: the reference theme's images carry no width/height and a
gallery's carry no alt, which is below the output floor conventions.md states.
Nothing can supply either yet — dimensions need the image read, and a filename is
not alt text. Queue 13 computes dimensions and brings structured items with it.
This commit is contained in:
+45
-10
@@ -11,10 +11,12 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
|
||||
"khosra/internal/content"
|
||||
)
|
||||
@@ -113,15 +115,44 @@ type Renderer struct {
|
||||
partials *template.Template
|
||||
md goldmark.Markdown
|
||||
style template.CSS
|
||||
// files is the site root, handed to features through Origin. Nil when there is none.
|
||||
files fs.FS
|
||||
}
|
||||
|
||||
// Partial renders a named fragment with the arguments a feature parsed. A feature under internal/ext is
|
||||
// handed one of these at wiring time, because markup belongs to the theme and a feature must not write any
|
||||
// (ADR-0036).
|
||||
// Partial renders a named fragment. A feature under internal/ext is handed one of these at wiring time,
|
||||
// because markup belongs to the theme and a feature must not write any (ADR-0036).
|
||||
type Partial func(name string, data Fragment) ([]byte, error)
|
||||
|
||||
// Fragment is what a fragment template receives (ADR-0037).
|
||||
type Fragment struct {
|
||||
// Args are the call's key="value" pairs, exactly as written. Escaping is the template's.
|
||||
Args map[string]string
|
||||
// Items is a list the feature gathered rather than the author wrote — the filenames a gallery found.
|
||||
// Kept apart from Args so a supplied value can never be mistaken for an authored one.
|
||||
Items []string
|
||||
}
|
||||
|
||||
// Origin tells a feature which bundle is being rendered, so a path in a call can resolve relative to it.
|
||||
//
|
||||
// Arguments are strings because that is what a shortcode call carries. A feature needing richer data is
|
||||
// the reason to widen this, not a reason to have made it `any` in advance.
|
||||
type Partial func(name string, args map[string]string) ([]byte, error)
|
||||
// Features read it from the parser context with OriginFrom. It carries the site's fs.FS rather than a
|
||||
// directory name alone, because every read goes through the rooted filesystem and never a joined path
|
||||
// (ADR-0031).
|
||||
type Origin struct {
|
||||
// Dir is the bundle's directory, relative to the site root: "content/comics/the-long-monsoon".
|
||||
Dir string
|
||||
// Files is the site root. Nil when the renderer was built without one, in which case a feature that
|
||||
// needs files degrades rather than guessing.
|
||||
Files fs.FS
|
||||
}
|
||||
|
||||
// originKey identifies the Origin in a parse. Unexported, so the typed accessor is the only way in.
|
||||
var originKey = parser.NewContextKey()
|
||||
|
||||
// OriginFrom reports the bundle being rendered, and false outside a bundle render.
|
||||
func OriginFrom(pc parser.Context) (Origin, bool) {
|
||||
origin, ok := pc.Get(originKey).(Origin)
|
||||
return origin, ok
|
||||
}
|
||||
|
||||
// New parses the theme and prepares the Markdown converter.
|
||||
//
|
||||
@@ -149,7 +180,7 @@ func New(siteFS fs.FS, extend func(Partial) []goldmark.Extender) (*Renderer, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := &Renderer{page: page, list: list, partials: partials, style: css}
|
||||
r := &Renderer{page: page, list: list, partials: partials, style: css, files: siteFS}
|
||||
// The typographer smooths quotes, dashes and ellipses in authored prose and leaves code spans alone,
|
||||
// because it works on the parsed tree rather than the text. That is the only change the engine makes to
|
||||
// an author's words (ADR-0034), and it is a parser option rather than a render transform, so it does
|
||||
@@ -167,12 +198,12 @@ func New(siteFS fs.FS, extend func(Partial) []goldmark.Extender) (*Renderer, err
|
||||
|
||||
// Partial renders one named fragment. A missing template is an error the caller degrades on, never a
|
||||
// failed request (extensions.md rule 5).
|
||||
func (r *Renderer) Partial(name string, args map[string]string) ([]byte, error) {
|
||||
func (r *Renderer) Partial(name string, data Fragment) ([]byte, error) {
|
||||
if r.partials.Lookup(name) == nil {
|
||||
return nil, fmt.Errorf("no template named %q", name)
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if err := r.partials.ExecuteTemplate(&out, name, args); err != nil {
|
||||
if err := r.partials.ExecuteTemplate(&out, name, data); err != nil {
|
||||
return nil, fmt.Errorf("partial %s: %w", name, err)
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
@@ -226,8 +257,12 @@ func readStyle(siteFS fs.FS) (template.CSS, error) {
|
||||
// exists in; both feed canonical and hreflang, which a theme must not construct itself. seq is the series
|
||||
// the bundle sits in, or nil.
|
||||
func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, seq *content.Sequence) ([]byte, error) {
|
||||
// The parse carries which bundle it is, so a feature can resolve a path in a call against the bundle's
|
||||
// own directory (ADR-0031: through the rooted filesystem, never a joined path).
|
||||
pc := parser.NewContext()
|
||||
pc.Set(originKey, Origin{Dir: path.Dir(b.Path), Files: r.files})
|
||||
var body bytes.Buffer
|
||||
if err := r.md.Convert(b.Body, &body); err != nil {
|
||||
if err := r.md.Convert(b.Body, &body, parser.WithContext(pc)); err != nil {
|
||||
return nil, fmt.Errorf("markdown %s: %w", b.Path, err)
|
||||
}
|
||||
title := b.Title
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
{{define "figure" -}}
|
||||
<figure>
|
||||
<img src="{{.src}}" alt="{{.alt}}">
|
||||
{{- if .caption}}
|
||||
<figcaption>{{.caption}}</figcaption>
|
||||
<img src="{{.Args.src}}" alt="{{.Args.alt}}">
|
||||
{{- if .Args.caption}}
|
||||
<figcaption>{{.Args.caption}}</figcaption>
|
||||
{{- end}}
|
||||
</figure>
|
||||
{{- end}}
|
||||
|
||||
{{define "gallery" -}}
|
||||
{{if .Items -}}
|
||||
<div class="gallery">
|
||||
{{- range .Items}}
|
||||
<figure><img src="{{.}}" alt=""></figure>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
Reference in New Issue
Block a user