diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml index b03a797..609601d 100644 --- a/.github/workflows/jekyll.yml +++ b/.github/workflows/jekyll.yml @@ -57,6 +57,7 @@ jobs: python3 tests/sync-cloudflare-security-headers.test.py bundle exec ruby tests/research-tag-collision.test.rb bundle exec ruby tests/asset-fingerprints.test.rb + bundle exec ruby tests/responsive-images.test.rb - name: Build site run: bash scripts/build.sh --skip-deps diff --git a/_plugins/responsive_images.rb b/_plugins/responsive_images.rb index 08cc4bd..5d7a57b 100644 --- a/_plugins/responsive_images.rb +++ b/_plugins/responsive_images.rb @@ -54,10 +54,38 @@ def local_asset_path(value) def apply_dimensions(img, info) return unless info - return if img['width'] && img['height'] - img['width'] = info['width'].to_s - img['height'] = info['height'].to_s + existing_width = img['width'] + existing_height = img['height'] + return if existing_width && existing_height + + natural_w = info['width'].to_f + natural_h = info['height'].to_f + return if natural_w <= 0 || natural_h <= 0 + + # Author set neither: emit both intrinsic dims for CLS. + if existing_width.nil? && existing_height.nil? + img['width'] = info['width'].to_s + img['height'] = info['height'].to_s + return + end + + # Numeric width without height: keep width, scale height. + if existing_width && existing_height.nil? + if existing_width.match?(/\A\d+(\.\d+)?\z/) + scaled = (existing_width.to_f * natural_h / natural_w).round + img['height'] = scaled.to_s if scaled.positive? + end + # Non-numeric width (e.g. 75%): leave alone; never invent height. + return + end + + # Numeric height without width: keep height, scale width. + return unless existing_height && existing_width.nil? + return unless existing_height.match?(/\A\d+(\.\d+)?\z/) + + scaled = (existing_height.to_f * natural_w / natural_h).round + img['width'] = scaled.to_s if scaled.positive? end def apply_loading(img) diff --git a/assets/css/research-v2.css b/assets/css/research-v2.css index 7954d48..b9d1f2b 100644 --- a/assets/css/research-v2.css +++ b/assets/css/research-v2.css @@ -623,6 +623,7 @@ /* Static images in the paper list sit at full width, paper-style */ .research-content img:not([src*="img.shields.io"]) { max-width: 100%; + height: auto; border-radius: var(--r-sm); margin: var(--s-3) 0; } diff --git a/tests/responsive-images.test.rb b/tests/responsive-images.test.rb new file mode 100644 index 0000000..5017bef --- /dev/null +++ b/tests/responsive-images.test.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'jekyll' +require 'nokogiri' +require_relative '../_plugins/responsive_images' + +def assert(condition, message) + raise message unless condition +end + +def img_node(html) + Nokogiri::HTML.fragment(html).at_css('img') +end + +INFO = { 'width' => 2391, 'height' => 2090 }.freeze + +# Neither attr: set both intrinsic dims (CLS / first-paint intent of #147). +bare = img_node('') +CoMPhyResponsiveImages.apply_dimensions(bare, INFO) +assert(bare['width'] == '2391', 'bare img should get natural width') +assert(bare['height'] == '2090', 'bare img should get natural height') + +# Author percentage width, no height: never overwrite width or invent height. +pct = img_node( + '' +) +CoMPhyResponsiveImages.apply_dimensions(pct, INFO) +assert(pct['width'] == '75%', 'percentage width must be preserved') +assert(pct['height'].nil?, 'percentage width must not get a pixel height') + +# Numeric width without height: keep width, scale height. +num = img_node( + '' +) +CoMPhyResponsiveImages.apply_dimensions(num, INFO) +assert(num['width'] == '800', 'numeric author width must be preserved') +expected_h = ((800.0 * 2090) / 2391).round +assert( + num['height'] == expected_h.to_s, + "expected proportional height #{expected_h}, got #{num['height']}" +) + +# Both already set: leave untouched. +both = img_node( + '' +) +CoMPhyResponsiveImages.apply_dimensions(both, INFO) +assert(both['width'] == '400', 'existing width must not change') +assert(both['height'] == '300', 'existing height must not change') + +# Numeric height without width: keep height, scale width. +h_only = img_node( + '' +) +CoMPhyResponsiveImages.apply_dimensions(h_only, INFO) +assert(h_only['height'] == '1045', 'numeric author height must be preserved') +expected_w = ((1045.0 * 2391) / 2090).round +assert( + h_only['width'] == expected_w.to_s, + "expected proportional width #{expected_w}, got #{h_only['width']}" +) + +# Nil info is a no-op. +noop = img_node('') +CoMPhyResponsiveImages.apply_dimensions(noop, nil) +assert(noop['width'].nil? && noop['height'].nil?, 'nil info must no-op') + +puts 'responsive-images.test.rb: all assertions passed'