Module: Abqari::RenderContext::PictureHelpers

Included in:
Abqari::RenderContext
Defined in:
lib/abqari/render_context/picture_helpers.rb

Overview

Image / icon / asset-path helpers. Lifted out of RenderContext so the main class can stay focused on templating; the behaviour is unchanged. Every method that emits markup returns already-safe HTML — templates interpolate via <%==.

Constant Summary collapse

HEROICON_VARIANTS =
{
  outline: '24/outline',
  solid:   '24/solid',
  mini:    '20/solid',
  micro:   '16/solid'
}.freeze

Instance Method Summary collapse

Instance Method Details

#asset_path(name) ⇒ Object



17
18
19
# File 'lib/abqari/render_context/picture_helpers.rb', line 17

def asset_path(name)
  site.assets.path_for(name)
end

#bundle_asset_url(path) ⇒ Object

Resolve a relative bundle-asset filename to an absolute URL by prepending the current page's URL. Passes through absolute paths (/foo), full URLs (https://…), and anchors (#foo) unchanged.

Used in layouts to render frontmatter image fields that point at colocated assets — e.g. image: hero.jpg in a post or workshop frontmatter. Markdown body images get auto-rewritten by the build's post-processor, but frontmatter values bypass that pass; this helper closes the gap.

Pair with sanitized_url for safe HTML output: <img src="<%= sanitized_url(bundle_asset_url(hero)) %>" …>



90
91
92
# File 'lib/abqari/render_context/picture_helpers.rb', line 90

def bundle_asset_url(path)
  bundle_asset_url_for(page, path)
end

#bundle_asset_url_for(item, path) ⇒ Object

Like bundle_asset_url, but resolves relative paths against the given item's URL rather than the current page. Used by listing partials (post list, related items, etc.) that render image fields for many items while the active page is the index.



99
100
101
102
103
104
105
# File 'lib/abqari/render_context/picture_helpers.rb', line 99

def bundle_asset_url_for(item, path)
  s = path.to_s
  return s if s.empty? || s.start_with?('/', 'http://', 'https://', '#')

  base = item.respond_to?(:url) ? item.url : item.to_s
  "#{base}#{s}"
end

#bundle_picture_tag(page, image, alt:, class_: nil, loading: 'eager', fetchpriority: 'high') ⇒ Object

Bundle-aware renderer. Three paths, tried in order:

1. **Pipeline path (preferred).** When `images.optimise` is
 on AND the bundle image is registered with `ImagePipeline`
 (i.e. it's a raster format the pipeline handles), this
 delegates to the pipeline's `picture_tag_for_url`, which
 emits the same `<picture>` shape as auto-rewritten
 markdown `<img>` tags get. Widths/quality/format come
 from `images:` config — no per-call configuration
 needed.

2. **Hand-rolled sibling fallback.** SVGs and any raster
 image whose author manually pre-generated `<basename>.avif`
 and `<basename>.webp` next to the source. Useful for
 sites that haven't migrated to the pipeline yet, or for
 cases where you want explicit control over a specific
 hero image.

3. **Plain `<img>`.** Last resort when neither of the above
 applies (e.g. SVGs, which don't benefit from format
 negotiation).


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/abqari/render_context/picture_helpers.rb', line 128

def bundle_picture_tag(page, image, alt:, class_: nil, loading: 'eager', fetchpriority: 'high')
  img_url = sanitized_url(bundle_asset_url_for(page, image))

  # Path 1 — pipeline. Skip for SVGs (vector, nothing to thumbnail).
  unless image.to_s.end_with?('.svg')
    extra = { class: class_, loading: loading }
    extra[:fetchpriority] = fetchpriority if fetchpriority
    pipeline_html = site.images.picture_tag_for_url(
      bundle_asset_url_for(page, image),
      alt: alt,
      attrs: extra.compact
    )
    return pipeline_html if pipeline_html
  end

  # Path 2 + 3 — hand-rolled siblings or plain <img>.
  attrs = []
  attrs << %(class="#{h(class_)}")            if class_
  attrs << %(src="#{h(img_url)}")
  attrs << %(alt="#{h(alt)}")
  attrs << %(loading="#{loading}")
  attrs << %(fetchpriority="#{fetchpriority}") if fetchpriority
  attrs << 'decoding="async"'
  plain_img = "<img #{attrs.join(' ')}>"

  return plain_img if image.to_s.end_with?('.svg')
  return plain_img unless page.respond_to?(:bundle_dir) && page.bundle_dir

  basename = File.basename(image.to_s, File.extname(image.to_s))
  avif_path = File.join(page.bundle_dir, "#{basename}.avif")
  webp_path = File.join(page.bundle_dir, "#{basename}.webp")
  avif_exists = File.exist?(avif_path)
  webp_exists = File.exist?(webp_path)

  return plain_img unless avif_exists || webp_exists

  sources = []
  if avif_exists
    avif_url = sanitized_url(bundle_asset_url_for(page, "#{basename}.avif"))
    sources << %(<source type="image/avif" srcset="#{h(avif_url)}">)
  end
  if webp_exists
    webp_url = sanitized_url(bundle_asset_url_for(page, "#{basename}.webp"))
    sources << %(<source type="image/webp" srcset="#{h(webp_url)}">)
  end

  "<picture>#{sources.join}#{plain_img}</picture>"
end

#heroicon(name, variant: :outline, **attrs) ⇒ Object

Inline a Heroicon SVG. Reads from vendor/heroicons// .svg at render time and inlines it directly — no extra HTTP request, no webfont, no CSP changes (everything is same- origin static markup). Color and size flow through CSS.

Variants map to the Heroicons folder layout:

:outline → 24/outline (24×24 stroke, default)
:solid   → 24/solid   (24×24 filled)
:mini    → 20/solid   (20×20 filled, smaller UI)
:micro   → 16/solid   (16×16 filled, very small UI)

Returns '' (renders nothing) when the icon name isn't found, so a typo doesn't fail the build — just an empty span. List of icon names: https://heroicons.com/



64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/abqari/render_context/picture_helpers.rb', line 64

def heroicon(name, variant: :outline, **attrs)
  subdir = HEROICON_VARIANTS[variant.to_sym] or return ''
  # Heroicons are engine-bundled — they ship in the gem.
  path = File.join(site.engine_root, 'vendor', 'heroicons', subdir, "#{name}.svg")
  return '' unless File.exist?(path)

  svg = File.read(path, encoding: 'UTF-8')
  return svg if attrs.empty?

  attrs_str = attrs.map { |k, v| %(#{h(k.to_s)}="#{h(v.to_s)}") }.join(' ')
  svg.sub(/<svg([^>]*)>/) { "<svg#{::Regexp.last_match(1)} #{attrs_str}>" }
end

#icon(name, **attrs) ⇒ Object

Site-local SVG icon. Hybrid path — a site can drop app/icons/<name>.svg to override an engine-bundled icon or add its own.



39
40
41
42
43
44
45
46
47
48
# File 'lib/abqari/render_context/picture_helpers.rb', line 39

def icon(name, **attrs)
  path = site.find_in_paths(File.join('app', 'icons', "#{name}.svg"))
  return '' unless File.exist?(path)

  svg = File.read(path, encoding: 'UTF-8')
  return svg if attrs.empty?

  attrs_str = attrs.map { |k, v| %(#{h(k.to_s)}="#{h(v.to_s)}") }.join(' ')
  svg.sub(/<svg([^>]*)>/) { "<svg#{::Regexp.last_match(1)} #{attrs_str}>" }
end

#optional_asset_path(name) ⇒ Object

Like asset_path, but returns nil instead of raising when the asset doesn't exist. Used to conditionally include opt-in site- local assets like app/assets/css/site.css — sites declare one when they want to add overrides on top of theme + _common.css; most sites don't, so the link should silently disappear.



26
27
28
29
30
# File 'lib/abqari/render_context/picture_helpers.rb', line 26

def optional_asset_path(name)
  site.assets.path_for(name)
rescue ArgumentError
  nil
end

#picture_tag(source, alt: '', sizes: nil, widths: nil) ⇒ Object



32
33
34
# File 'lib/abqari/render_context/picture_helpers.rb', line 32

def picture_tag(source, alt: '', sizes: nil, widths: nil)
  site.images.picture_tag(source, alt: alt, sizes: sizes, widths: widths)
end