Commit Diff


commit - 5a365c2607655acb416397319c5c362e1bbb4f15
commit + c3f5671b4eb04fba2cae977039bcf15265cb3b0e
blob - 8d00dd266fc4970b94aee4cc71061253377837bc
blob + 35887bf88046dd1ff00fb21ee83241b4d208ace8
--- README.md
+++ README.md
@@ -1,176 +1,532 @@
 # Набор шаблонов для сайтов Hugo от Radium
 
-## Подключение темы на сайте
-Настройте [слияние](https://gohugo.io/configuration/introduction/#merge-configuration-settings) конфигурации в hugo.toml, указав:
+## Подключение темы
 
+Настройте [слияние конфигурации](https://gohugo.io/configuration/introduction/#merge-configuration-settings) в `hugo.toml`:
+
 ```toml
 _merge = 'deep'
 ```
 
+Это позволяет переопределять параметры, заданные темой, в конфигурации сайта.
+
 ## Параметры
-Все используемые параметры должны содержаться в настройках сайта в .Params.radium
 
-publisher
-: ссылка на профиль публикатора для элементов JSON-LD.
-external_rel
-: rel по умолчанию для абсолютных ссылок.
+Все параметры темы находятся в пространстве имён `radium`:
 
-## render hooks
+```text
+.Params.radium
+```
+
+Параметры сайта доступны через:
+
+```go-html-template
+site.Params.radium
+```
+
+Параметры также могут быть переопределены для отдельной страницы через front matter.
+
+### Общие параметры
+
+`publisher`
+: ссылка на профиль издателя для элементов JSON-LD.
+
+`external_rel`
+: значение `rel` по умолчанию для абсолютных ссылок.
+
+### Изображения
+
+Параметры обработки изображений находятся в:
+
+```text
+.Params.radium.images
+```
+
+`widths`
+: набор ширин для генерации адаптивных изображений.
+
+`sizes`
+: значение атрибута `sizes` по умолчанию.
+
+`mode`
+: режим обработки изображения: `auto`, `lossy` или `lossless`.
+
+Параметры могут задаваться:
+
+1. в конфигурации темы;
+2. в конфигурации сайта;
+3. во front matter страницы;
+4. непосредственно при вызове `image`.
+
+Более специфичное значение имеет приоритет.
+
+## Render hooks
+
 ### render-link
-Вставляет ссылку с разрешённым назначением ссылки.
-Для абсолютных ссылок указывает rel из настроек сайта или значение по умолчанию `noopener noreferrer external`.
 
+Вставляет ссылку с разрешённым набором атрибутов.
+
+Для абсолютных ссылок устанавливает `rel` из настроек сайта или значение по умолчанию:
+
+```text
+noopener noreferrer external
+```
+
 ### render-blockquote
-[Вставляет](https://gohugo.io/render-hooks/blockquotes/) блок цитаты с указанием источника и заголовка, а также блок alert.
 
+[Вставляет](https://gohugo.io/render-hooks/blockquotes/) блок цитаты с указанием источника и заголовка, а также поддерживает блоки alert.
+
 ## Шаблоны
+
 ### baseof
-Базовый шаблон, включающий индексирование Pagefind только для main тега.
-Для тега html устанавливается язык и направление, если оно явно прописано в настройках языка.
-В шаблоне подключаются partial head, header, footer.
-Блок main должен быть переопределён в отдельных шаблонах.
-Блок head может быть переопределён в отдельных шаблонах, если требуется добавить что-то в тэг head.
 
+Базовый шаблон сайта.
+
+Включает индексирование Pagefind только для элемента `main`.
+
+Для элемента `html` устанавливает язык и направление текста, если направление явно указано в настройках языка.
+
+Подключает partials:
+
+* `head`;
+* `header`;
+* `footer`.
+
+Блок `main` должен быть переопределён в дочерних шаблонах.
+
+Блок `head` может быть переопределён, если требуется добавить дополнительные элементы в `<head>`.
+
 ## Partials
 
 ### attrs
-Рендерит словарь из ключей и значений в виде атрибутов тэга.
-Для булевых значений вставляется или не вставляется соответствующий ключ без значения.
 
+Преобразует `dict` атрибутов в строку HTML-атрибутов.
+
+Обычные значения выводятся в виде:
+
+```html
+class="example"
+```
+
+Булевы значения обрабатываются как HTML boolean attributes:
+
+* `true` — выводится только имя атрибута;
+* `false` — атрибут не выводится.
+
+Например:
+
+```go-html-template
+{{ partial "attrs.html" (dict
+  "class" "video"
+  "controls" true
+  "autoplay" false
+) }}
+```
+
+создаёт:
+
+```html
+class="video" controls
+```
+
 ### pick
-Отбирает из dict ключи, входящие в массив allowed.
 
+Возвращает новый `dict`, содержащий только ключи из переданного массива `allowed`.
+
+Например:
+
+```go-html-template
+{{- $attributes := partial "pick.html" (dict
+  "dict" .Params
+  "allowed" (slice "class" "id")
+) -}}
+```
+
+Значения `false`, `0` и пустые строки сохраняются.
+
 ### image
-Отображает изображение, передаваемое в .image.
-.image должен быть ресурсом, полученным через .Resources.Get* или resources.Get*.
 
-Для SVG изображений производит сжатие, а для всех остальных указывает ширину и высоту.
+Отображает изображение, переданное в параметре `image`.
 
-Если установлен .alt, то устанавливает атрибут alt.
+`image` должен быть Hugo image resource, например полученным через:
 
+```go-html-template
+.Resources.Get
+resources.Get
+resources.GetRemote
+```
+
+Для изображений, которые Hugo умеет обрабатывать, partial создаёт адаптивые варианты изображения.
+
+Лесенка размеров определяется параметром `widths`. Если он не передан, используются настройки страницы или сайта из:
+
+```text
+radium.images.widths
+```
+
+Размеры больше исходного изображения не создаются. Исходная ширина при этом всегда добавляется в `srcset`.
+
+Например, для исходного изображения шириной `4000px` и лесенки:
+
+```text
+480 768 1024 1440 1920
+```
+
+будут доступны варианты:
+
+```text
+480 768 1024 1440 1920 4000
+```
+
+Для исходного изображения шириной `1300px`:
+
+```text
+480 768 1024 1300
+```
+
+#### Lossy-изображения
+
+Для lossy-изображений создаются:
+
+* AVIF;
+* WebP.
+
+В HTML используется `<picture>`, где AVIF является предпочтительным форматом, а WebP — fallback.
+
+JPEG автоматически считается lossy.
+
+#### Lossless-изображения
+
+Для lossless-изображений создаётся lossless WebP.
+
+PNG и BMP автоматически считаются lossless.
+
+Для форматов, режим которых нельзя однозначно определить по MIME-типу, следует явно передать:
+
+```text
+mode = "lossy"
+```
+
+или:
+
+```text
+mode = "lossless"
+```
+
+#### Исходный формат
+
+Если исходное изображение уже находится в целевом формате и используется в исходном разрешении, оно не перекодируется повторно.
+
+#### SVG и другие необрабатываемые изображения
+
+Изображения, которые Hugo не умеет преобразовывать, передаются без изменения.
+
+SVG дополнительно минифицируется.
+
+Для SVG можно вручную передавать `width` и `height` через `attributes`.
+
+#### Атрибуты
+
+Дополнительные атрибуты `<img>` передаются через `attributes`:
+
+```go-html-template
+{{- partial "image.html" (dict
+  "image" $image
+  "page" .
+  "attributes" (dict
+    "alt" "Описание изображения"
+    "class" "photo"
+    "loading" "lazy"
+    "decoding" "async"
+  )
+) -}}
+```
+
+Если `width` и `height` не заданы, partial указывает размеры автоматически, когда Hugo может их определить.
+
+Если передан только один из этих атрибутов, второй вычисляется с сохранением соотношения сторон.
+
+Параметр `sizes` можно передать непосредственно:
+
+```go-html-template
+{{- partial "image.html" (dict
+  "image" $image
+  "page" .
+  "sizes" "(max-width: 900px) 100vw, 900px"
+) -}}
+```
+
 ### logo
+
 Отображает логотип со ссылкой.
 
 Параметры:
 
-- image - имя ресурса сайта с логотипом (img/logo.svg)
-- class - класс ссылки, содержащей логотип (logo)
-- link - ссылка, которая будет указана для логотипа (главная страница с учётом языка)
-- alt - атрибут alt изображения (Logo image)
+`image`
+: имя ресурса сайта с логотипом. По умолчанию `img/logo.svg`.
 
-### head/favicon, head/apple-touch-icon
-Связывают страницу с имеющимися favicon или apple-touch-icon.
-Для поиска favicon применяется маска {,**/}favicon.*, а для apple-touch-icon - {,**/}apple-touch-icon*.png
+`class`
+: класс ссылки, содержащей логотип. По умолчанию `logo`.
 
+`link`
+: ссылка логотипа. По умолчанию главная страница с учётом языка.
+
+`alt`
+: значение атрибута `alt`. По умолчанию `Logo image`.
+
+### head/favicon
+
+Связывает страницу с найденными favicon.
+
+Для поиска используется маска:
+
+```text
+{,**/}favicon.*
+```
+
+SVG-файлы минифицируются.
+
+Если Hugo может определить размеры растрового изображения, у `<link>` устанавливается атрибут `sizes`.
+
+### head/apple-touch-icon
+
+Связывает страницу с найденными Apple Touch Icon.
+
+Для поиска используется маска:
+
+```text
+{,**/}apple-touch-icon*.png
+```
+
 ### head/manifest
-Связывает страницу с manifest.json, находящимся в корне assets/
 
+Связывает страницу с `manifest.json`, находящимся в корне `assets/`.
+
 ### head/css
-Подключает css/main.css
 
+Подключает:
+
+```text
+css/main.css
+```
+
 ### head/js
-Подключает js/main.js
 
+Подключает:
+
+```text
+js/main.js
+```
+
 ### head/pagefind
-Подключение стилей и скрипта, если окружение не является разработкой.
 
+Подключает стили и скрипт Pagefind, если окружение не является development.
+
 ### head/alternate
-Указывает link alternate для других языков и форматом страницы.
 
+Добавляет `<link rel="alternate">` для других языков и форматов страницы.
+
 ### head/base
-Добавляет title, description и canonical в заголовок, а также meta charset + viewport.
 
+Добавляет в `<head>`:
+
+* `title`;
+* `description`;
+* canonical URL;
+* `meta charset`;
+* `viewport`.
+
 ### head/social
-Добавляет рендер встроенных шаблонов OpenGraph и Twitter Cards.
 
+Добавляет встроенные шаблоны Open Graph и Twitter Cards.
+
 ### schema
-Подключает JSON-LD схемы для связанных объектов.
 
-Принимает в контексте либо страницу, либо dict с ключами page и schema.
-Схема - это строка или массив строк.
+Подключает JSON-LD-схемы связанных объектов.
 
-Подключение в head:
+В контекст можно передать:
+
+* страницу;
+* `dict` с ключами `page` и `schema`.
+
+`schema` может быть строкой или массивом строк.
+
+Пример:
+
 ```go-html-template
 {{- partial "schema/json-ld.html" . | safeHTML }}
 ```
-или
+
+С явным указанием схемы:
+
 ```go-html-template
-{{- partial "schema/json-ld.html" (dict "page" . "schema" "BreadcrumbList") | safeHTML }}
+{{- partial "schema/json-ld.html" (dict
+  "page" .
+  "schema" "BreadcrumbList"
+) | safeHTML }}
 ```
 
-Если в Page Bundle есть файл <тип>.jsonld (например, Person.jsonld), он будет добавлен в список schema.
+Если в Page Bundle находится файл `<тип>.jsonld`, например:
 
-Если по итогу для страницы список schema будет пустым, то будет отрендерен [встроенный](https://gohugo.io/templates/embedded/#schema) шаблон.
+```text
+Person.jsonld
+```
 
+соответствующая схема автоматически добавляется в список.
+
+Если в результате список схем пуст, используется [встроенный шаблон Hugo](https://gohugo.io/templates/embedded/#schema).
+
 #### Статья
-Для указания publisher в статье укажите publisher в Params страницы, либо в настройках сайта.
 
+Для указания `publisher` укажите его в параметрах страницы либо в настройках сайта.
+
 ## Shortcodes
+
 ### include
-Позволяет [вставить](https://gohugo.io/render-hooks/blockquotes/#pageinner-details) другой markdown файл в текущий.
-Полезно при разделении частей страницы на несколько файлов.
 
+Позволяет [вставить](https://gohugo.io/render-hooks/blockquotes/#pageinner-details) другой Markdown-файл в текущий.
+
+Полезно для разделения большой страницы на несколько файлов.
+
 ### details
-Корректный details, позволяющий рендерить html.
 
-Аналогично [стандартному](https://gohugo.io/shortcodes/details/#article) shortcode, за исключением способа вызова — необходимо использовать вызов {{% details %}} для рендера внутреннего текста в качестве Markdown.
+Создаёт `<details>` с возможностью рендеринга внутреннего содержимого как HTML/Markdown.
 
+Аналогичен [стандартному shortcode Hugo](https://gohugo.io/shortcodes/details/#article), но для рендеринга внутреннего содержимого как Markdown следует использовать notation:
+
+```text
+{{% details %}}
+```
+
 ### a
-Создаёт ссылку с атрибутами href, class, id, rel.
 
-Полезно для установки rel.
+Создаёт ссылку `<a>`.
 
-Вызывает partial/element.
-Если необходимы другие атрибуты, то переопределите этот shortcode.
+Поддерживаются атрибуты:
 
+* `href`;
+* `title`;
+* `rel`;
+* `target`;
+* `class`;
+* `id`;
+* `download`;
+* `referrerpolicy`;
+* `hreflang`;
+* `type`;
+* `role`;
+* `tabindex`;
+* `aria-label`;
+* `aria-current`;
+* `aria-describedby`.
+
+Пример:
+
+```md
+{{< a href="/file.pdf" download=true rel="nofollow" >}}
+Скачать
+{{< /a >}}
+```
+
 ### section
-Создаёт секцию с атрибутами class, id.
 
-Поддерживает стандартную и markdown нотацию вызова.
+Создаёт элемент `<section>`.
 
+Поддерживаются атрибуты:
+
+* `class`;
+* `id`.
+
+Поддерживает обычную и Markdown-нотацию shortcode.
+
 ### video
-Вставляет видео.
 
-Поддерживает параметры:
-- class
-- poster
-- loading
-- height
-- width
-- src
-- tabindex
-- area-hidden
+Создаёт элемент `<video>`.
 
-В тэг вставляются следующие параметры, только если в атрибутах не передан false:
-- autoplay
-- loop
-- muted
-- playsinline
-- disablepictureinpicture
-- disableremoteplayback
+Поддерживаются обычные атрибуты:
 
-Если передать параметр controls с любым значением, кроме false, то будет вставлен этот атрибут.
+* `class`;
+* `poster`;
+* `height`;
+* `width`;
+* `src`;
+* `tabindex`;
+* `aria-hidden`.
 
+Следующие boolean attributes включены по умолчанию:
+
+* `autoplay`;
+* `loop`;
+* `muted`;
+* `playsinline`;
+* `disablepictureinpicture`;
+* `disableremoteplayback`.
+
+Их можно отключить, передав boolean `false`.
+
+`controls` по умолчанию отключён и может быть включён значением `true`.
+
+Например:
+
 ```md
-{{< video tabindex="-1" area-hidden="true" poster="https://peach.blender.org/wp-content/uploads/title_anouncement.jpg?x11217">}}
-{{< source codecs="avc1.4d002a">}}
+{{< video
+  tabindex="-1"
+  aria-hidden="true"
+  controls=true
+  poster="https://peach.blender.org/wp-content/uploads/title_anouncement.jpg?x11217"
+>}}
+{{< source codecs="avc1.4d002a" >}}
 https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4
 {{< /source >}}
 {{< /video >}}
 ```
 
+Для boolean attributes рекомендуется передавать настоящие булевы значения без кавычек:
+
+```text
+controls=true
+autoplay=false
+```
+
+а не:
+
+```text
+controls="true"
+autoplay="false"
+```
+
 ### source
-Вставляет source для тэга video.
-Ссылку можно вставить параметром src или передав внутри тэга.
 
-Поддерживается вставка параметров:
-- srcset
-- sizes
-- media
-- width
-- height
-- type
+Создаёт `<source>`, предназначенный прежде всего для использования внутри `<video>`.
 
-Если type не задан, то тип будет определён автоматически.
+URL можно передать параметром `src`:
 
-При указании параметра codecs, он будет вставлен в type.
+```md
+{{< source src="/video.mp4" >}}
+```
+
+или внутренним содержимым shortcode:
+
+```md
+{{< source >}}
+/video.mp4
+{{< /source >}}
+```
+
+Поддерживаются атрибуты:
+
+* `srcset`;
+* `sizes`;
+* `media`;
+* `width`;
+* `height`.
+
+`src`, `type` и `codecs` обрабатываются отдельно.
+
+Если `type` не указан, shortcode пытается определить MIME-тип ресурса автоматически.
+
+При указании `codecs` значение добавляется к `type`, например:
+
+```html
+type="video/mp4; codecs=avc1.4d002a"
+```
blob - /dev/null
blob + 4c66731acc32bddd340dbc2d4cf063670ae1cb91 (mode 644)
--- /dev/null
+++ config/_default/params.toml
@@ -0,0 +1,4 @@
+[radium.images]
+widths = [480, 768, 1024, 1440, 1920]
+sizes = "100vw"
+mode = "auto"
blob - 00cb92cb03bf4a2f58a2ee978a74f1225ca819d8
blob + cbf1202e44f25e6afa8801855925e80f45231689
--- layouts/_partials/image.html
+++ layouts/_partials/image.html
@@ -1,19 +1,286 @@
-{{- $img := .image }}
-{{- $width := .width }}
-{{- $height := .height }}
-{{- if eq $img.MediaType.SubType "svg" }}
-  {{- $img = $img | minify }}
-{{- else }}
-  {{- $width = $width or $img.Width }}
-  {{- $height = $height or $img.Height }}
-{{- end }}
-<img
-  src="{{ $img.RelPermalink }}"
-  {{- with $width }}
-  width="{{ . }}"
-  {{- end }}
-  {{- with $height }}
-  height="{{ . }}"
-  {{- end }}
-  {{ with .alt }}alt="{{ . }}"{{ end }}
->
+{{- $image := .image -}}
+{{- $page := .page -}}
+{{- $attributes := .attributes | default dict -}}
+
+{{- if not $image -}}
+  {{- errorf "image.html: image is required" -}}
+{{- end -}}
+
+{{- if not (reflect.IsImageResource $image) -}}
+  {{- errorf "image.html: %q is not an image resource" $image.Name -}}
+{{- end -}}
+
+
+{{/* Processable raster image */}}
+
+{{- if reflect.IsImageResourceProcessable $image -}}
+
+  {{/* Configuration: site -> page -> invocation */}}
+
+  {{- $config := site.Params.radium.images -}}
+
+  {{- with $page -}}
+    {{- with .Params.radium.images -}}
+      {{- $config = merge $config . -}}
+    {{- end -}}
+  {{- end -}}
+
+  {{- $widths := .widths | default $config.widths -}}
+  {{- $sizes := .sizes | default $config.sizes -}}
+  {{- $mode := .mode | default $config.mode | lower -}}
+
+
+  {{/* Normalize configured widths */}}
+
+  {{- $normalized := slice -}}
+
+  {{- range $widths -}}
+    {{- $width := int . -}}
+
+    {{- if gt $width 0 -}}
+      {{- $normalized = $normalized | append $width -}}
+    {{- end -}}
+  {{- end -}}
+
+  {{- $widths = collections.Sort (collections.Uniq $normalized) -}}
+
+  {{- if not $widths -}}
+    {{- errorf "image.html: width ladder is empty" -}}
+  {{- end -}}
+
+
+  {{/*
+    Build the actual ladder.
+
+    Never upscale configured variants, but always include
+    the original width.
+
+    1300 + [480 768 1024 1440 1920]
+      -> [480 768 1024 1300]
+
+    4000 + [480 768 1024 1440 1920]
+      -> [480 768 1024 1440 1920 4000]
+  */}}
+
+  {{- $targets := slice -}}
+
+  {{- range $widths -}}
+    {{- if le . $image.Width -}}
+      {{- $targets = $targets | append . -}}
+    {{- end -}}
+  {{- end -}}
+
+  {{- $targets = $targets | append $image.Width -}}
+  {{- $targets = collections.Sort (collections.Uniq $targets) -}}
+
+
+  {{/*
+    src gets the largest configured width that does not
+    exceed the original.
+
+    The original itself remains available through srcset.
+  */}}
+
+  {{- $fallbackWidth := $image.Width -}}
+  {{- $maxWidth := index $widths (sub (len $widths) 1) -}}
+
+  {{- if gt $image.Width $maxWidth -}}
+    {{- $fallbackWidth = $maxWidth -}}
+  {{- end -}}
+
+
+  {{/* Compression mode */}}
+
+  {{- if eq $mode "auto" -}}
+
+    {{- if eq $image.MediaType.Type "image/jpeg" -}}
+      {{- $mode = "lossy" -}}
+
+    {{- else if in
+      (slice "image/png" "image/bmp")
+      $image.MediaType.Type
+    -}}
+      {{- $mode = "lossless" -}}
+
+    {{- else -}}
+      {{- errorf
+        "image.html: cannot determine compression mode of %q (%s); specify mode"
+        $image.Name
+        $image.MediaType.Type
+      -}}
+    {{- end -}}
+
+  {{- else if not (in (slice "lossy" "lossless") $mode) -}}
+
+    {{- errorf
+      "image.html: invalid mode %q; expected auto, lossy, or lossless"
+      $mode
+    -}}
+
+  {{- end -}}
+
+
+  {{/* WebP variants */}}
+
+  {{- $webp := partial "image/variants.html" (dict
+    "image" $image
+    "widths" $targets
+    "format" "webp"
+    "mode" $mode
+  ) -}}
+
+  {{- $fallback := index
+    $webp.variants
+    (printf "%d" $fallbackWidth)
+  -}}
+
+
+  {{/* Width / height */}}
+
+  {{- $defaults := dict "alt" "" -}}
+
+  {{- $hasWidth := isset $attributes "width" -}}
+  {{- $hasHeight := isset $attributes "height" -}}
+
+  {{- if and (not $hasWidth) (not $hasHeight) -}}
+
+    {{- $defaults = merge $defaults (dict
+      "width" $fallback.Width
+      "height" $fallback.Height
+    ) -}}
+
+  {{- else if and $hasWidth (not $hasHeight) -}}
+
+    {{- $width := int $attributes.width -}}
+    {{- $height := int (math.Round (mul
+      $fallback.Height
+      (div (float $width) $fallback.Width)
+    )) -}}
+
+    {{- $defaults = merge $defaults (dict
+      "height" $height
+    ) -}}
+
+  {{- else if and $hasHeight (not $hasWidth) -}}
+
+    {{- $height := int $attributes.height -}}
+    {{- $width := int (math.Round (mul
+      $fallback.Width
+      (div (float $height) $fallback.Height)
+    )) -}}
+
+    {{- $defaults = merge $defaults (dict
+      "width" $width
+    ) -}}
+
+  {{- end -}}
+
+
+  {{/* User attributes override defaults.
+       Generated resource attributes override everything. */}}
+
+  {{- $attributes = merge
+    $defaults
+    $attributes
+    (dict
+      "src" $fallback.RelPermalink
+      "srcset" $webp.srcset
+      "sizes" $sizes
+    )
+  -}}
+
+
+  {{/* Lossy: AVIF + WebP fallback */}}
+
+  {{- if eq $mode "lossy" -}}
+
+    {{- $avif := partial "image/variants.html" (dict
+      "image" $image
+      "widths" $targets
+      "format" "avif"
+      "mode" "lossy"
+    ) -}}
+
+    <picture>
+      <source{{ partial "attrs.html" (dict
+        "type" "image/avif"
+        "srcset" $avif.srcset
+        "sizes" $sizes
+      ) }}>
+      <img{{ partial "attrs.html" $attributes }}>
+    </picture>
+
+  {{- else -}}
+
+    <img{{ partial "attrs.html" $attributes }}>
+
+  {{- end -}}
+
+
+{{/* Non-processable image */}}
+
+{{- else -}}
+
+  {{/* SVG can still be minified. */}}
+
+  {{- if eq $image.MediaType.Type "image/svg+xml" -}}
+    {{- $image = resources.Minify $image -}}
+  {{- end -}}
+
+  {{- $defaults := dict "alt" "" -}}
+
+
+  {{/*
+    Some non-processable formats have readable dimensions.
+    SVG and ICO, for example, don't.
+  */}}
+
+  {{- if reflect.IsImageResourceWithMeta $image -}}
+
+    {{- $hasWidth := isset $attributes "width" -}}
+    {{- $hasHeight := isset $attributes "height" -}}
+
+    {{- if and (not $hasWidth) (not $hasHeight) -}}
+
+      {{- $defaults = merge $defaults (dict
+        "width" $image.Width
+        "height" $image.Height
+      ) -}}
+
+    {{- else if and $hasWidth (not $hasHeight) -}}
+
+      {{- $width := int $attributes.width -}}
+      {{- $height := int (math.Round (mul
+        $image.Height
+        (div (float $width) $image.Width)
+      )) -}}
+
+      {{- $defaults = merge $defaults (dict
+        "height" $height
+      ) -}}
+
+    {{- else if and $hasHeight (not $hasWidth) -}}
+
+      {{- $height := int $attributes.height -}}
+      {{- $width := int (math.Round (mul
+        $image.Width
+        (div (float $height) $image.Height)
+      )) -}}
+
+      {{- $defaults = merge $defaults (dict
+        "width" $width
+      ) -}}
+
+    {{- end -}}
+
+  {{- end -}}
+
+  {{- $attributes = merge
+    $defaults
+    $attributes
+    (dict "src" $image.RelPermalink)
+  -}}
+
+  <img{{ partial "attrs.html" $attributes }}>
+
+{{- end -}}
blob - /dev/null
blob + cf14416326313a72f0b52d536393e2e8b916abea (mode 644)
--- /dev/null
+++ layouts/_partials/image/variants.html
@@ -0,0 +1,44 @@
+{{- $variants := dict -}}
+{{- $srcset := slice -}}
+{{- $mediaType := printf "image/%s" .format -}}
+
+{{- range .widths -}}
+  {{- $width := int . -}}
+  {{- $resource := $.image -}}
+
+  {{- $sameWidth := eq $width $.image.Width -}}
+  {{- $sameFormat := eq $.image.MediaType.Type $mediaType -}}
+
+  {{- if not (and $sameWidth $sameFormat) -}}
+    {{- if $sameWidth -}}
+      {{- $resource = $.image.Process (printf
+        "%s %s"
+        $.format
+        $.mode
+      ) -}}
+    {{- else -}}
+      {{- $resource = $.image.Process (printf
+        "resize %dx %s %s"
+        $width
+        $.format
+        $.mode
+      ) -}}
+    {{- end -}}
+  {{- end -}}
+
+  {{- $variants = merge $variants (dict
+    (printf "%d" $width)
+    $resource
+  ) -}}
+
+  {{- $srcset = $srcset | append (printf
+    "%s %dw"
+    $resource.RelPermalink
+    $resource.Width
+  ) -}}
+{{- end -}}
+
+{{- return (dict
+  "variants" $variants
+  "srcset" (delimit $srcset ", ")
+) -}}