Skip to content

Commit 63f5156

Browse files
committed
Filter content to add srcset and sizes
This changes our approach from one where `srcset` and `sizes` are written to the image markup in post content to one where the responsive attributes are added on the front end when the page is being rendered. There is a small performance hit but the benefit is worth the tradeoff for what is gained. What exactly do we gain? Glad you asked: * Automatically extend support to images in posts published before we responsive image support was available. * Removes the need to update the markup in TinyMCE when the image size is changed. * Keeps `sizes` attributes, which should probably be adjusted based on the needs of the theme, from being stored in the database. * Is non-destructive if a site needs to change available image sizes and regenerate thumbnails. Changes: * Removes the old JS code for handling markup changes in TinyMCE * Adds `tevkori_filter_content_images()` as a display filter. * Adds `_tevkori_filter_content_images_callback()` to process filtered images. * Adds tests
1 parent 057bd7b commit 63f5156

File tree

3 files changed

+184
-136
lines changed

3 files changed

+184
-136
lines changed

js/wp-tevko-responsive-images.js

Lines changed: 0 additions & 53 deletions
This file was deleted.

tests/test-suite.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,4 +371,74 @@ function test_tevkori_filter_attachment_image_attributes_thumbnails() {
371371
$this->assertFalse( isset( $resp_attr['srcset'] ) );
372372
$this->assertFalse( isset( $resp_attr['sizes'] ) );
373373
}
374+
375+
/**
376+
* @group 170
377+
*/
378+
function test_tevkori_filter_content_images() {
379+
// Make image.
380+
$id = $this->_test_img();
381+
382+
$srcset = tevkori_get_srcset_string( $id, 'medium' );
383+
$sizes = tevkori_get_sizes_string( $id, 'medium' );
384+
385+
// Function used to build HTML for the editor.
386+
$img = get_image_tag( $id, '', '', '', 'medium' );
387+
$img_no_size = str_replace( 'size-', '', $img );
388+
$img_no_size_id = str_replace( 'wp-attachment-', '', $img_no_size );
389+
390+
// Manually add srcset and sizes to the markup from get_image_tag();
391+
$respimg = preg_replace('|<img ([^>]+) />|', '<img $1 ' . $srcset . ' ' . $sizes . ' />', $img);
392+
$respimg_no_size = preg_replace('|<img ([^>]+) />|', '<img $1 ' . $srcset . ' ' . $sizes . ' />', $img_no_size);
393+
$respimg_no_size_id = preg_replace('|<img ([^>]+) />|', '<img $1 ' . $srcset . ' ' . $sizes . ' />', $img_no_size_id);
394+
395+
$content = '<p>Welcome to WordPress! This post contains important information. After you read it, you can make it private to hide it from visitors but still have the information handy for future reference.</p>
396+
<p>First things first:</p>
397+
398+
%1$s
399+
400+
<ul>
401+
<li><a href="http://wordpress.org" title="Subscribe to the WordPress mailing list for Release Notifications">Subscribe to the WordPress mailing list for release notifications</a></li>
402+
</ul>
403+
404+
%2$s
405+
406+
<p>As a subscriber, you will receive an email every time an update is available (and only then). This will make it easier to keep your site up to date, and secure from evildoers.<br />
407+
When a new version is released, <a href="http://wordpress.org" title="If you are already logged in, this will take you directly to the Dashboard">log in to the Dashboard</a> and follow the instructions.<br />
408+
Upgrading is a couple of clicks!</p>
409+
410+
%3$s
411+
412+
<p>Then you can start enjoying the WordPress experience:</p>
413+
<ul>
414+
<li>Edit your personal information at <a href="http://wordpress.org" title="Edit settings like your password, your display name and your contact information">Users &#8250; Your Profile</a></li>
415+
<li>Start publishing at <a href="http://wordpress.org" title="Create a new post">Posts &#8250; Add New</a> and at <a href="http://wordpress.org" title="Create a new page">Pages &#8250; Add New</a></li>
416+
<li>Browse and install plugins at <a href="http://wordpress.org" title="Browse and install plugins at the official WordPress repository directly from your Dashboard">Plugins &#8250; Add New</a></li>
417+
<li>Browse and install themes at <a href="http://wordpress.org" title="Browse and install themes at the official WordPress repository directly from your Dashboard">Appearance &#8250; Add New Themes</a></li>
418+
<li>Modify and prettify your website&#8217;s links at <a href="http://wordpress.org" title="For example, select a link structure like: http://example.com/1999/12/post-name">Settings &#8250; Permalinks</a></li>
419+
<li>Import content from another system or WordPress site at <a href="http://wordpress.org" title="WordPress comes with importers for the most common publishing systems">Tools &#8250; Import</a></li>
420+
<li>Find answers to your questions at the <a href="http://wordpress.orgs" title="The official WordPress documentation, maintained by the WordPress community">WordPress Codex</a></li>
421+
</ul>';
422+
423+
$content_unfiltered = sprintf( $content, $img, $img_no_size, $img_no_size_id );
424+
$content_filtered = sprintf( $content, $respimg, $respimg_no_size, $respimg_no_size_id );
425+
426+
$this->assertSame( $content_filtered, tevkori_filter_content_images( $content_unfiltered ) );
427+
}
428+
429+
/**
430+
* @group 170
431+
*/
432+
function test_tevkori_filter_content_images_with_preexisting_srcset() {
433+
// Make image.
434+
$id = $this->_test_img();
435+
436+
// Generate HTML and add a dummy srcset attribute.
437+
$image_html = get_image_tag( $id, '', '', '', 'medium' );
438+
$image_html = preg_replace('|<img ([^>]+) />|', '<img $1 ' . 'srcset="image2x.jpg 2x" />', $image_html );
439+
440+
// The content filter should return the image unchanged.
441+
$this->assertSame( $image_html, tevkori_filter_content_images( $image_html ) );
442+
}
443+
374444
}

wp-tevko-responsive-images.php

Lines changed: 114 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -297,31 +297,130 @@ function tevkori_get_src_sizes( $id, $size = 'thumbnail' ) {
297297
}
298298

299299
/**
300-
* Filter for extending image tag to include srcset attribute.
300+
* Filters images in post content to add `srcset` and `sizes`.
301301
*
302-
* @see images_send_to_editor
303-
* @return string HTML for image.
302+
* @since 3.0
303+
*
304+
* @param string $content The raw post content to be filtered.
305+
* @return string Converted content with `srcset` and `sizes` added to images.
304306
*/
305-
function tevkori_extend_image_tag( $html, $id, $caption, $title, $align, $url, $size, $alt ) {
306-
add_filter( 'editor_max_image_size', 'tevkori_editor_image_size' );
307+
function tevkori_filter_content_images( $content ) {
308+
// Only match images in our uploads directory.
309+
$uploads_dir = wp_upload_dir();
310+
$path_to_upload_dir = $uploads_dir['baseurl'];
307311

308-
// Get the srcset attribute.
309-
$srcset = tevkori_get_srcset_string( $id, $size );
312+
$content = preg_replace_callback( '|<img ([^>]+' . $path_to_upload_dir . '[^>]+)[\s?][\/?]>|i', '_tevkori_filter_content_images_callback', $content );
310313

311-
remove_filter( 'editor_max_image_size', 'tevkori_editor_image_size' );
314+
return $content;
315+
}
316+
add_filter( 'the_content', 'tevkori_filter_content_images', 5, 1 );
312317

313-
if ( $srcset ) {
318+
/**
319+
* Private preg_replace callback used in tevkori_filter_content_images()
320+
*
321+
* @access private
322+
* @since 3.0.0
323+
*/
324+
function _tevkori_filter_content_images_callback( $image ) {
325+
if ( empty( $image ) ) {
326+
return false;
327+
}
314328

315-
// Build the data-sizes attribute if sizes were returned.
316-
$sizes = tevkori_get_sizes( $id, $size );
317-
$sizes = $sizes ? 'data-sizes="' . $sizes . '"' : '';
329+
list( $image_html, $atts ) = $image;
330+
$id = $size = false;
318331

319-
$html = preg_replace( '/(src\s*=\s*"(.+?)")/', '$1 ' . $sizes . ' ' . $srcset, $html );
332+
// Bail early if a `srcset` attribute already exists.
333+
if ( false !== strpos( $atts, 'srcset=' ) ) {
334+
return $image_html;
320335
}
321336

322-
return $html;
337+
// Grab ID and size info from core classes.
338+
if ( preg_match( '/wp-image-([0-9]+)/i', $atts, $class_id ) ) {
339+
(int) $id = $class_id[1];
340+
}
341+
if ( preg_match( '/size-([^\s|"]+)/i', $atts, $class_size ) ) {
342+
$size = $class_size[1];
343+
}
344+
345+
if ( $id && false === $size ) {
346+
preg_match( '/width="([0-9]+)"/', $atts, $width );
347+
preg_match( '/height="([0-9]+)"/', $atts, $height );
348+
349+
$size = array(
350+
(int) $width[1],
351+
(int) $height[1]
352+
);
353+
}
354+
355+
/*
356+
* If attempts to get values for `id` and `size` failed, use the
357+
* src to query for matching values in `_wp_attachment_metadata`
358+
*/
359+
if ( false === $id || false === $size ) {
360+
preg_match( '/src="([^"]+)"/', $atts, $url );
361+
362+
if ( ! $url[1] ) {
363+
return $image_html;
364+
}
365+
366+
$image_filename = basename( $url[1] );
367+
368+
/*
369+
* If we already have an ID, we use it to get the attachment metadata
370+
* using `wp_get_attachment_metadata()`. Otherwise, we'll use the image
371+
* `src` url to query the postmeta table for both the attachement ID and
372+
* metadata, which we'll use later to get the size.
373+
*/
374+
if ( ! empty( $id ) ) {
375+
$meta = wp_get_attachment_metadata( $id );
376+
} else {
377+
global $wpdb;
378+
$meta_object = $wpdb->get_row( $wpdb->prepare(
379+
"SELECT `post_id`, `meta_value` FROM $wpdb->postmeta WHERE `meta_key` = '_wp_attachment_metadata' AND `meta_value` LIKE %s",
380+
'%' . $image_filename . '%'
381+
) );
382+
383+
// If the query is successful, we can determine the ID and size.
384+
if ( is_object( $meta_object ) ) {
385+
$id = $meta_object->post_id;
386+
// Unserialize the meta_value returned in our query.
387+
$meta = maybe_unserialize( $meta_object->meta_value );
388+
} else {
389+
$meta = false;
390+
}
391+
}
392+
393+
/*
394+
* Now that we have the attachment ID and metadata, we can retrieve the
395+
* size by matching the original image's `src` filename with the sizes
396+
* included in the attachment metadata.
397+
*/
398+
if ( $id && $meta ) {
399+
/*
400+
* First, see if the file is the full size image. If not, we loop through
401+
* the itermediate sizes until we find a match.
402+
*/
403+
if ( $image_filename === basename( $meta['file'] ) ) {
404+
$size = 'full';
405+
} else {
406+
foreach( $meta['sizes'] as $image_size => $image_size_data ) {
407+
if ( $image_filename === $image_size_data['file'] ) {
408+
$size = $image_size;
409+
break;
410+
}
411+
}
412+
}
413+
}
414+
}
415+
416+
// If we have an ID and size, try for srcset and sizes and update the markup.
417+
if ( $id && $size && $srcset = tevkori_get_srcset_string( $id, $size ) ) {
418+
$sizes = tevkori_get_sizes_string( $id, $size );
419+
$image_html = "<img " . $atts . " " . $srcset . " " . $sizes . " />";
420+
};
421+
422+
return $image_html;
323423
}
324-
add_filter( 'image_send_to_editor', 'tevkori_extend_image_tag', 0, 8 );
325424

326425
/**
327426
* Filter to add srcset and sizes attributes to post thumbnails and gallery images.
@@ -351,71 +450,3 @@ function tevkori_filter_attachment_image_attributes( $attr, $attachment, $size )
351450
return $attr;
352451
}
353452
add_filter( 'wp_get_attachment_image_attributes', 'tevkori_filter_attachment_image_attributes', 0, 3 );
354-
355-
/**
356-
* Disable the editor size constraint applied for images in TinyMCE.
357-
*
358-
* @return array A width & height array so large it shouldn't constrain reasonable images.
359-
*/
360-
function tevkori_editor_image_size() {
361-
return array( 99999, 99999 );
362-
}
363-
364-
/**
365-
* Load admin scripts.
366-
*
367-
* @param string $hook Admin page file name.
368-
*/
369-
function tevkori_load_admin_scripts( $hook ) {
370-
if ( $hook == 'post.php' || $hook == 'post-new.php' ) {
371-
wp_enqueue_script( 'wp-tevko-responsive-images', plugin_dir_url( __FILE__ ) . 'js/wp-tevko-responsive-images.js', array( 'wp-backbone' ), '2.0.0', true );
372-
}
373-
}
374-
add_action( 'admin_enqueue_scripts', 'tevkori_load_admin_scripts' );
375-
376-
377-
/**
378-
* Filter for the_content to replace data-size attributes with size attributes.
379-
*
380-
* @since 2.2.0
381-
*
382-
* @param string $content The raw post content to be filtered.
383-
*/
384-
function tevkori_filter_content_sizes( $content ) {
385-
$images = '/(<img\s.*?)data-sizes="([^"]+)"/i';
386-
$sizes = '${2}';
387-
388-
return preg_replace( $images, '${1}sizes="' . $sizes . '"', $content );
389-
}
390-
add_filter( 'the_content', 'tevkori_filter_content_sizes' );
391-
392-
393-
/**
394-
* Ajax handler for updating the srcset when an image is changed in the editor.
395-
*
396-
* @since 2.3.0
397-
*
398-
* @return string A sourcest value.
399-
*/
400-
function tevkori_ajax_srcset() {
401-
402-
// Bail early if no post ID is passed.
403-
if ( empty( $_POST['postID'] ) ) {
404-
return;
405-
}
406-
407-
$postID = (int) $_POST['postID'];
408-
if ( ! $postID ) {
409-
return;
410-
}
411-
412-
// Grab the image size being passed from the AJAX request.
413-
$size = empty( $_POST['size'] ) ? $_POST['size'] : '';
414-
415-
// Get the srcset value for our image.
416-
$srcset = tevkori_get_srcset( $postID, $size );
417-
418-
// For AJAX requests, we echo the result and then die.
419-
wp_send_json( $srcset );
420-
}
421-
add_action( 'wp_ajax_tevkori_ajax_srcset', 'tevkori_ajax_srcset' );

0 commit comments

Comments
 (0)